Welcome to FlowZap, the App to diagram with Speed, Clarity and Control.

MiroFish: Build Your Own Synthetic Focus Group

4/12/2026

Tags: MiroFish, synthetic focus groups, multi-agent systems, AI agents, simulation, LLM architecture

Jules Kovac

Jules Kovac

Business Analyst, Founder

MiroFish: Build Your Own Synthetic Focus Group

What if you could run a focus group at 2am, with 50 opinionated AI agents, against any document you throw at them — and get back a ranked list of objections before your morning coffee?

That is the core idea behind MiroFish. It is an open-source multi-agent simulation engine that spawns LLM-powered agents with distinct personas, gives them a seed document to react to, and lets them interact on a simulated social platform across multiple rounds. What emerges is not a prediction — it is a synthetic public opinion simulation. Think digital ethnography, not forecasting.

A few things to set straight before you get excited:

  • License is AGPL v3.0, not MIT. Embedding this inside a closed commercial SaaS means you must open-source your modifications. For internal tooling, research, or a consulting workflow, you are fine.
  • This is a scenario simulator, not an oracle. It will not tell you next quarter's churn rate. It will tell you which persona types are most bothered by your pricing page.
  • Token costs are non-trivial. 50 agents × 20 rounds = a lot of LLM calls. Start local with Ollama or a budget endpoint like DeepInfra (~$0.20/1M tokens).

For installation, the README covers everything: github.com/666ghj/MiroFish. You need Node.js 18+, Python 3.11–3.12, uv, and an LLM API key. Docker Compose is available if you want to skip the setup pain.

 

How It Actually Works (System Architecture)

Here is what happens under the hood when you hit Launch. Three participants are involved: your browser, the MiroFish stack (a React frontend on :3000 and a Python/OASIS backend on :5001), and the LLM API you have wired up.

browser { # Your Browser
n1: circle label="Start"
n2: rectangle label="Paste seed doc
& configure agents"
n3: rectangle label="Launch"
n10: rectangle label="View discussion
logs & insights"
n11: circle label="Done"
n1.handle(right) -> n2.handle(left)
n2.handle(right) -> n3.handle(left)
n3.handle(bottom) -> mirofish.n4.handle(top) [label="POST /simulation/start"]
n10.handle(right) -> n11.handle(left)
}

mirofish { # MiroFish Stack (:3000 / :5001)
n4: rectangle label="Spawn N agents
with personas"
n5: rectangle label="Distribute seed doc
to each agent"
n6: rectangle label="Orchestrate
interaction rounds"
n9: rectangle label="Aggregate logs
& prepare results"
n4.handle(right) -> n5.handle(left)
n5.handle(right) -> n6.handle(left)
n6.handle(bottom) -> llm.n7.handle(top) [label="Parallel agent
prompts"]
n9.handle(top) -> browser.n10.handle(bottom) [label="Results JSON"]
}

llm { # LLM API (OpenAI / Ollama / DeepInfra)
n7: rectangle label="Run inference
per agent"
n8: rectangle label="Return agent
response"
n7.handle(right) -> n8.handle(left)
loop [per round] n7 n8
n8.handle(top) -> mirofish.n9.handle(bottom) [label="Agent outputs"]
}

Notice the LLM lane: it is called per agent, per round, in parallel. At 50 agents and 10 rounds, that is 500 LLM calls for one simulation run. Now you see why token cost is a real concern.

 

Scenario 1: Pre-Launch Copy Validation

Your landing page is written. You think it is good. MiroFish lets you torture-test it against 50 synthetic versions of your ICP before you spend a dollar on ads.

Three participants here: you, the MiroFish engine managing the simulation, and the agent swarm doing the actual reading and debating.

builder { # Builder
n1: circle label="Start"
n2: rectangle label="Write copy draft
+ define personas"
n3: rectangle label="Submit to
MiroFish"
n12: rectangle label="Read objections
report"
n13: circle label="Refine & iterate"
n1.handle(right) -> n2.handle(left)
n2.handle(right) -> n3.handle(left)
n3.handle(bottom) -> engine.n4.handle(top) [label="POST /simulation/start"]
n12.handle(right) -> n13.handle(left)
}

engine { # MiroFish Engine
n4: rectangle label="Initialize simulation
& spawn agents"
n5: rectangle label="Distribute seed doc
+ persona config"
n10: rectangle label="Collect & rank
top objections"
n11: rectangle label="Build report"
n4.handle(right) -> n5.handle(left)
n5.handle(bottom) -> agents.n6.handle(top) [label="Seed doc + persona"]
n10.handle(right) -> n11.handle(left)
n11.handle(top) -> builder.n12.handle(bottom) [label="Top 3 objections"]
}

agents { # Agent Swarm (50 LLM Agents)
n6: rectangle label="Read copy
& form opinion"
n7: rectangle label="Post to
simulated platform"
n8: rectangle label="Read peers'
reactions"
n9: rectangle label="Update memory
& stance"
n6.handle(right) -> n7.handle(left)
n7.handle(right) -> n8.handle(left)
n8.handle(right) -> n9.handle(left)
loop [10 rounds] n6 n7 n8 n9
n9.handle(top) -> engine.n10.handle(bottom) [label="Round outputs"]
}

The agent loop is the heart of it — agents post opinions, read each other's reactions, and update their stance every round. By round 10, dominant opinion clusters have formed. The engine ranks them and sends you back the top objections.

Sample persona config for a B2B SaaS product:

{
  "seed_document": "Your landing page copy here...",
  "agent_count": 50,
  "personas": [
    { "type": "skeptical_cto", "weight": 0.2 },
    { "type": "budget_conscious_pm", "weight": 0.3 },
    { "type": "early_adopter", "weight": 0.2 },
    { "type": "enterprise_buyer", "weight": 0.3 }
  ],
  "max_rounds": 10
}

Post-processing the output: Raw output is a JSON discussion log. Pipe it through an LLM with a prompt like: From this discussion log, extract the 3 most common objections to the pricing, the most praised feature, and the most requested missing capability.

 

Scenario 2: Mid-Simulation Crisis Injection

This is where MiroFish gets genuinely interesting. You can pause a running simulation at any round, inject a news event, a competitor announcement, or a policy change, and watch how agents re-evaluate everything they already believe.

This shows the full three-participant chain: you trigger the injection, the engine broadcasts it, and the agents re-debate with the new context baked into their memory.

builder { # Builder
n1: circle label="Start"
n2: rectangle label="Define baseline
scenario + seed doc"
n3: rectangle label="Launch
baseline simulation"
n9: rectangle label="Inject crisis
event"
n15: rectangle label="Compare delta:
before vs after"
n16: circle label="End"
n1.handle(right) -> n2.handle(left)
n2.handle(right) -> n3.handle(left)
n3.handle(bottom) -> engine.n4.handle(top) [label="POST /simulation/start"]
n9.handle(bottom) -> engine.n10.handle(top) [label="POST /simulation/{id}/inject"]
n15.handle(right) -> n16.handle(left)
}

engine { # MiroFish Engine
n4: rectangle label="Initialize
baseline simulation"
n5: rectangle label="Dispatch agents
for rounds 1–5"
n8: rectangle label="Baseline captured
— signal builder"
n10: rectangle label="Receive crisis
variable"
n11: rectangle label="Broadcast event
to agents"
n14: rectangle label="Aggregate
post-crisis logs"
n4.handle(right) -> n5.handle(left)
n5.handle(bottom) -> agents.n6.handle(top) [label="Run 5 rounds"]
n8.handle(top) -> builder.n9.handle(bottom) [label="Baseline ready — inject now"]
n10.handle(right) -> n11.handle(left)
n11.handle(bottom) -> agents.n12.handle(top) [label="Inject: crisis context"]
n14.handle(top) -> builder.n15.handle(bottom) [label="Post-crisis discussion logs"]
}

agents { # Agent Swarm
n6: rectangle label="Run baseline
rounds 1–5"
n7: rectangle label="Baseline
complete"
n12: rectangle label="Absorb crisis
context"
n13: rectangle label="Re-debate with
new information"
n6.handle(right) -> n7.handle(left)
n7.handle(top) -> engine.n8.handle(bottom) [label="Baseline outputs"]
n12.handle(right) -> n13.handle(left)
n13.handle(top) -> engine.n14.handle(bottom) [label="Post-crisis outputs"]
}

Injection payload example:

{
  "event_type": "market_event",
  "content": "Breaking: a major competitor just launched a fully free alternative with 80% feature parity.",
  "inject_at_round": 5
}

The sentiment delta between pre- and post-injection tells you which persona types are most vulnerable — and by how much. That is your competitive risk map.

 

Scenario 3: A/B Offer Testing

Two loyalty program mechanics. Two simulations. One winner. No waiting four weeks for real A/B results.

pm { # Product Manager
n1: circle label="Start"
n2: rectangle label="Define Variant A
& Variant B"
n3: rectangle label="Submit Variant A
for simulation"
n7: rectangle label="Submit Variant B
for simulation"
n11: rectangle label="Compare results:
pick the winner"
n12: circle label="Ship it"
n1.handle(right) -> n2.handle(left)
n2.handle(right) -> n3.handle(left)
n3.handle(bottom) -> mirofish.n4.handle(top) [label="POST /simulation/start (A)"]
n7.handle(bottom) -> mirofish.n8.handle(top) [label="POST /simulation/start (B)"]
n11.handle(right) -> n12.handle(left)
}

mirofish { # MiroFish Engine
n4: rectangle label="Simulate Variant A:
50 agents react"
n5: rectangle label="Agents evaluate
perceived value"
n6: rectangle label="Aggregate:
Variant A scores"
n8: rectangle label="Simulate Variant B:
50 agents react"
n9: rectangle label="Agents evaluate
perceived value"
n10: rectangle label="Aggregate:
Variant B scores"
n4.handle(right) -> n5.handle(left)
n5.handle(right) -> n6.handle(left)
n6.handle(top) -> pm.n7.handle(bottom) [label="Variant A: sentiment + intent"]
n8.handle(right) -> n9.handle(left)
n9.handle(right) -> n10.handle(left)
n10.handle(top) -> pm.n11.handle(bottom) [label="Variant B: sentiment + intent"]
}

What you compare across both runs: activation intent score, perceived-value rating, and churn-risk signal. If Variant A scores higher on activation but Variant B wins on perceived value, you have a segmentation decision — not a coin flip.

 

Before You Ship Anything

Checklist Item Why It Matters
Seed quality Dense, specific copy. Vague seeds produce vague agents.
Persona weights Mirror your real audience distribution so the synthetic debate is not distorted.
Start small Run 5–10 rounds first. Scale only after the output looks coherent.
Model choice Use local 7B or GPT-4o-mini for development. Save premium models for final validation runs.
Post-processing Always run logs through an LLM aggregator. Raw JSON is not insight.
Run a blank baseline Start with a neutral seed first to establish a sentiment floor.
AGPL Internal tool = fine. SaaS wrapper = open your code.

 

Visualizing These Diagrams

Paste any of the FlowZap Code blocks above, or save them as a .fz file, into your FlowZap project to render them as shareable visual diagrams for your team.

Stop dragging arrows. Engineer systems instead, with FlowZap.

 

Inspirations

Back to all Blog articles