- MiroFish-Offline Skill
- Skill by
- ara.so
- — Daily 2026 Skills collection.
- MiroFish-Offline is a fully local multi-agent swarm intelligence engine. Feed it any document (press release, policy draft, financial report) and it generates hundreds of AI agents with unique personalities that simulate public reaction on social media — posts, arguments, opinion shifts — hour by hour. No cloud APIs required: Neo4j CE 5.15 handles graph memory, Ollama serves the LLMs.
- Architecture Overview
- Document Input
- │
- ▼
- Graph Build (NER + relationship extraction via Ollama LLM)
- │
- ▼
- Neo4j Knowledge Graph (entities, relations, embeddings via nomic-embed-text)
- │
- ▼
- Env Setup (generate hundreds of agent personas with personalities + memory)
- │
- ▼
- Simulation (agents post, reply, argue, shift opinions on simulated platforms)
- │
- ▼
- Report (ReportAgent interviews focus group, queries graph, generates analysis)
- │
- ▼
- Interaction (chat with any individual agent, full memory persists)
- Backend
-
- Flask + Python 3.11
- Frontend
-
- Vue 3 + Node 18
- Graph DB
-
- Neo4j CE 5.15 (bolt protocol)
- LLM
-
- Ollama (OpenAI-compatible
- /v1
- endpoint)
- Embeddings
- :
- nomic-embed-text
- (768-dimensional, via Ollama)
- Search
- Hybrid — 0.7 × vector similarity + 0.3 × BM25 Installation Option A: Docker (Recommended) git clone https://github.com/nikmcfly/MiroFish-Offline.git cd MiroFish-Offline cp .env.example .env
Start Neo4j + Ollama + MiroFish backend + frontend
docker compose up -d
Pull required models into the Ollama container
docker exec mirofish-ollama ollama pull qwen2.5:32b docker exec mirofish-ollama ollama pull nomic-embed-text
Check all services are healthy
docker compose ps Open http://localhost:3000 . Option B: Manual Setup 1. Neo4j docker run -d --name neo4j \ -p 7474 :7474 -p 7687 :7687 \ -e NEO4J_AUTH = neo4j/mirofish \ neo4j:5.15-community 2. Ollama ollama serve & ollama pull qwen2.5:32b
Main LLM (~20GB, requires 24GB VRAM)
ollama pull qwen2.5:14b
Lighter option (~10GB VRAM)
ollama pull nomic-embed-text
Embeddings (small, fast)
- Backend cp .env.example .env
Edit .env (see Configuration section)
cd backend pip install -r requirements.txt python run.py
Backend starts on http://localhost:5000
- Frontend cd frontend npm install npm run dev
Frontend starts on http://localhost:3000
Configuration ( .env )
── LLM (Ollama OpenAI-compatible endpoint) ──────────────────────────
LLM_API_KEY
ollama LLM_BASE_URL = http://localhost:11434/v1 LLM_MODEL_NAME = qwen2.5:32b
── Neo4j ─────────────────────────────────────────────────────────────
NEO4J_URI
bolt://localhost:7687 NEO4J_USER = neo4j NEO4J_PASSWORD = mirofish
── Embeddings (Ollama) ───────────────────────────────────────────────
EMBEDDING_MODEL
nomic-embed-text EMBEDDING_BASE_URL = http://localhost:11434
── Optional: swap Ollama for any OpenAI-compatible provider ─────────
LLM_API_KEY=$OPENAI_API_KEY
LLM_BASE_URL=https://api.openai.com/v1
LLM_MODEL_NAME=gpt-4o
Core Python API GraphStorage Interface The abstraction layer between MiroFish and the graph database: from backend . storage . base import GraphStorage from backend . storage . neo4j_storage import Neo4jStorage
Initialize storage (typically done via Flask app.extensions)
storage
Neo4jStorage ( uri = os . environ [ "NEO4J_URI" ] , user = os . environ [ "NEO4J_USER" ] , password = os . environ [ "NEO4J_PASSWORD" ] , embedding_model = os . environ [ "EMBEDDING_MODEL" ] , embedding_base_url = os . environ [ "EMBEDDING_BASE_URL" ] , llm_base_url = os . environ [ "LLM_BASE_URL" ] , llm_api_key = os . environ [ "LLM_API_KEY" ] , llm_model = os . environ [ "LLM_MODEL_NAME" ] , ) Building a Knowledge Graph from a Document from backend . services . graph_builder import GraphBuilder builder = GraphBuilder ( storage = storage )
Feed a document string
with open ( "press_release.txt" , "r" ) as f : document_text = f . read ( )
Extract entities + relationships, store in Neo4j
graph_id
builder . build ( content = document_text , title = "Q4 Earnings Report" , source_type = "financial_report" , ) print ( f"Graph built: { graph_id } " )
Returns a graph_id used for subsequent simulation runs
Creating and Running a Simulation from backend . services . simulation import SimulationService sim = SimulationService ( storage = storage )
Create a simulation environment from an existing graph
sim_id
sim . create_environment ( graph_id = graph_id , agent_count = 200 ,
Number of agents to generate
simulation_hours
24 ,
Simulated time span
platform
"twitter" ,
"twitter" | "reddit" | "weibo"
)
Run the simulation (blocking — use async wrapper for production)
result
sim . run ( sim_id = sim_id ) print ( f"Simulation complete. Posts generated: { result [ 'post_count' ] } " ) print ( f"Sentiment trajectory: { result [ 'sentiment_over_time' ] } " ) Querying Simulation Results from backend . services . report import ReportAgent report_agent = ReportAgent ( storage = storage )
Generate a structured analysis report
report
report_agent . generate ( sim_id = sim_id , focus_group_size = 10 ,
Number of agents to interview
include_graph_search
True , ) print ( report [ "summary" ] ) print ( report [ "key_narratives" ] ) print ( report [ "sentiment_shift" ] ) print ( report [ "influential_agents" ] ) Chatting with a Simulated Agent from backend . services . agent_chat import AgentChatService chat = AgentChatService ( storage = storage )
List agents from a completed simulation
agents
chat . list_agents ( sim_id = sim_id , limit = 10 ) agent_id = agents [ 0 ] [ "id" ] print ( f"Chatting with: { agents [ 0 ] [ 'persona' ] [ 'name' ] } " ) print ( f"Personality: { agents [ 0 ] [ 'persona' ] [ 'traits' ] } " )
Send a message — agent responds in-character with full memory
response
chat . send ( agent_id = agent_id , message = "Why did you post that criticism about the earnings report?" , ) print ( response [ "reply" ] )
→ Agent responds using its personality, opinion bias, and post history
Hybrid Search on the Knowledge Graph from backend . services . search import SearchService search = SearchService ( storage = storage )
Hybrid search: 0.7 * vector similarity + 0.3 * BM25
results
search . query ( text = "executive compensation controversy" , graph_id = graph_id , top_k = 5 , vector_weight = 0.7 , bm25_weight = 0.3 , ) for r in results : print ( r [ "entity" ] , r [ "relationship" ] , r [ "score" ] ) Implementing a Custom GraphStorage Backend from backend . storage . base import GraphStorage from typing import List , Dict , Any class MyCustomStorage ( GraphStorage ) : """ Swap Neo4j for any graph DB by implementing this interface. Register via Flask app.extensions['neo4j_storage'] = MyCustomStorage(...) """ def store_entity ( self , entity : Dict [ str , Any ] ) -
str :
Store entity, return entity_id
raise NotImplementedError def store_relationship ( self , source_id : str , target_id : str , relation_type : str , properties : Dict [ str , Any ] , ) -
str : raise NotImplementedError def vector_search ( self , embedding : List [ float ] , top_k : int = 5 ) -
List [ Dict [ str , Any ] ] : raise NotImplementedError def keyword_search ( self , query : str , top_k : int = 5 ) -
List [ Dict [ str , Any ] ] : raise NotImplementedError def get_agent_memory ( self , agent_id : str ) -
Dict [ str , Any ] : raise NotImplementedError def update_agent_memory ( self , agent_id : str , memory_update : Dict [ str , Any ] ) -
None : raise NotImplementedError Flask App Integration Pattern
backend/app.py — how storage is wired via dependency injection
from flask import Flask from backend . storage . neo4j_storage import Neo4jStorage import os def create_app ( ) : app = Flask ( name )
Single storage instance, injected everywhere via app.extensions
storage
Neo4jStorage ( uri = os . environ [ "NEO4J_URI" ] , user = os . environ [ "NEO4J_USER" ] , password = os . environ [ "NEO4J_PASSWORD" ] , embedding_model = os . environ [ "EMBEDDING_MODEL" ] , embedding_base_url = os . environ [ "EMBEDDING_BASE_URL" ] , llm_base_url = os . environ [ "LLM_BASE_URL" ] , llm_api_key = os . environ [ "LLM_API_KEY" ] , llm_model = os . environ [ "LLM_MODEL_NAME" ] , ) app . extensions [ "neo4j_storage" ] = storage from backend . routes import graph_bp , simulation_bp , report_bp app . register_blueprint ( graph_bp ) app . register_blueprint ( simulation_bp ) app . register_blueprint ( report_bp ) return app Accessing Storage in a Flask Route from flask import Blueprint , current_app , request , jsonify simulation_bp = Blueprint ( "simulation" , name ) @simulation_bp . route ( "/api/simulation/run" , methods = [ "POST" ] ) def run_simulation ( ) : storage = current_app . extensions [ "neo4j_storage" ] data = request . json sim = SimulationService ( storage = storage ) sim_id = sim . create_environment ( graph_id = data [ "graph_id" ] , agent_count = data . get ( "agent_count" , 200 ) , simulation_hours = data . get ( "simulation_hours" , 24 ) , ) result = sim . run ( sim_id = sim_id ) return jsonify ( result ) REST API Reference Method Endpoint Description POST /api/graph/build Upload document, build knowledge graph GET /api/graph/:id Get graph entities and relationships POST /api/simulation/create Create simulation environment POST /api/simulation/run Execute simulation GET /api/simulation/:id/results Get posts, sentiment, metrics GET /api/simulation/:id/agents List generated agents POST /api/report/generate Generate ReportAgent analysis POST /api/agent/:id/chat Chat with a specific agent GET /api/search Hybrid search the knowledge graph Example: Build graph from document curl -X POST http://localhost:5000/api/graph/build \ -H "Content-Type: application/json" \ -d '{ "content": "Acme Corp announces record Q4 earnings, CFO resigns...", "title": "Q4 Press Release", "source_type": "press_release" }'
→
Example: Run a simulation curl -X POST http://localhost:5000/api/simulation/run \ -H "Content-Type: application/json" \ -d '{ "graph_id": "g_abc123", "agent_count": 150, "simulation_hours": 12, "platform": "twitter" }'
→
Hardware Selection Guide Use Case Model VRAM RAM Quick test / dev qwen2.5:7b 6 GB 16 GB Balanced quality qwen2.5:14b 10 GB 16 GB Production quality qwen2.5:32b 24 GB 32 GB CPU-only (slow) qwen2.5:7b None 16 GB Switch model by editing .env : LLM_MODEL_NAME = qwen2.5:14b Then restart the backend — no other changes needed. Common Patterns PR Crisis Test Pipeline import os from backend . storage . neo4j_storage import Neo4jStorage from backend . services . graph_builder import GraphBuilder from backend . services . simulation import SimulationService from backend . services . report import ReportAgent storage = Neo4jStorage ( uri = os . environ [ "NEO4J_URI" ] , user = os . environ [ "NEO4J_USER" ] , password = os . environ [ "NEO4J_PASSWORD" ] , embedding_model = os . environ [ "EMBEDDING_MODEL" ] , embedding_base_url = os . environ [ "EMBEDDING_BASE_URL" ] , llm_base_url = os . environ [ "LLM_BASE_URL" ] , llm_api_key = os . environ [ "LLM_API_KEY" ] , llm_model = os . environ [ "LLM_MODEL_NAME" ] , ) def test_press_release ( text : str ) -
dict :
1. Build knowledge graph
builder
GraphBuilder ( storage = storage ) graph_id = builder . build ( content = text , title = "Draft PR" , source_type = "press_release" )
2. Simulate public reaction
sim
SimulationService ( storage = storage ) sim_id = sim . create_environment ( graph_id = graph_id , agent_count = 300 , simulation_hours = 48 ) sim . run ( sim_id = sim_id )
3. Generate report
report
ReportAgent ( storage = storage ) . generate ( sim_id = sim_id , focus_group_size = 15 ) return { "sentiment_peak" : report [ "sentiment_over_time" ] [ 0 ] , "key_narratives" : report [ "key_narratives" ] , "risk_score" : report [ "risk_score" ] , "recommended_edits" : report [ "recommendations" ] , }
Usage
with open ( "draft_announcement.txt" ) as f : result = test_press_release ( f . read ( ) ) print ( f"Risk score: { result [ 'risk_score' ] } /10" ) print ( f"Top narrative: { result [ 'key_narratives' ] [ 0 ] } " ) Use Any OpenAI-Compatible Provider
Claude via Anthropic (or any proxy)
LLM_API_KEY
$ANTHROPIC_API_KEY LLM_BASE_URL = https://api.anthropic.com/v1 LLM_MODEL_NAME = claude-3-5-sonnet-20241022
OpenAI
LLM_API_KEY
$OPENAI_API_KEY LLM_BASE_URL = https://api.openai.com/v1 LLM_MODEL_NAME = gpt-4o
Local LM Studio
LLM_API_KEY
lm-studio LLM_BASE_URL = http://localhost:1234/v1 LLM_MODEL_NAME = your-loaded-model Troubleshooting Neo4j connection refused
Check Neo4j is running
docker ps | grep neo4j
Check bolt port
nc -zv localhost 7687
View Neo4j logs
docker logs neo4j --tail 50 Ollama model not found
List available models
ollama list
Pull missing models
ollama pull qwen2.5:32b ollama pull nomic-embed-text
Check Ollama is serving
curl http://localhost:11434/api/tags Out of VRAM
Switch to smaller model in .env
LLM_MODEL_NAME
qwen2.5:14b
or qwen2.5:7b
Restart backend
cd backend && python run.py Embeddings dimension mismatch
nomic-embed-text produces 768-dim vectors
If you switch embedding models, drop and recreate the Neo4j vector index:
In Neo4j browser (http://localhost:7474):
DROP INDEX entity_embedding IF EXISTS;
Then restart MiroFish — it recreates the index with correct dimensions.
Docker Compose: Ollama container can't access GPU
docker-compose.yml — add GPU reservation:
services : ollama : deploy : resources : reservations : devices : - driver : nvidia count : 1 capabilities : [ gpu ] Slow simulation on CPU Use qwen2.5:7b for faster (lower quality) inference Reduce agent_count to 50–100 for testing Reduce simulation_hours to 6–12 CPU inference with 7b model: expect ~5–10 tokens/sec Frontend can't reach backend
Check VITE_API_BASE_URL in frontend/.env
VITE_API_BASE_URL
http://localhost:5000
Verify backend is up
curl http://localhost:5000/api/health Project Structure MiroFish-Offline/ ├── backend/ │ ├── run.py # Entry point │ ├── app.py # Flask factory, DI wiring │ ├── storage/ │ │ ├── base.py # GraphStorage abstract interface │ │ └── neo4j_storage.py # Neo4j implementation │ ├── services/ │ │ ├── graph_builder.py # NER + relationship extraction │ │ ├── simulation.py # Agent simulation engine │ │ ├── report.py # ReportAgent + focus group │ │ ├── agent_chat.py # Per-agent chat interface │ │ └── search.py # Hybrid vector + BM25 search │ └── routes/ │ ├── graph.py │ ├── simulation.py │ └── report.py ├── frontend/ # Vue 3 (fully English UI) ├── docker-compose.yml ├── .env.example └── README.md