This guide is for the engineers, architects, and operators who deploy, extend, and maintain BettaFish. It documents the system's architecture, configuration surface, security posture, operational procedures, and internals in enough depth to run it reliably or rebuild it from scratch.
For installation and day-to-day usage, see the companion User Guide.
Table of Contents
- Executive Summary
- System Architecture
- Component Reference
- Requirements & Dependencies
- Configuration Mastery
- Data Layer & Schema
- HTTP API Reference
- Deployment
- Operations
- Security Analysis
- Code Patterns & Conventions
- Testing
- Extending the System
- Potential Improvements & Tech Debt
- Quick Reference
1. Executive Summary
BettaFish is a multi-agent public-opinion analysis platform written in pure Python. It orchestrates four LLM-driven "deep search" agents, a moderator LLM, an AI crawler, and a collection of local sentiment models to turn a natural-language question into a chart-rich analysis report.
Architectural character:
- Modular monolith. A single Flask + Socket.IO application (
app.py) supervises everything: it launches three Streamlit sub-applications as child processes, runs the ForumEngine in a background thread, and mounts the ReportEngine as a Flask blueprint. There is no microservice mesh and no message broker. - Hand-rolled agent framework. The agents do not use LangChain/LangGraph. Each engine implements its own node-graph (
nodes/,state/,llms/,prompts/,tools/) — the__init__describes it as a "framework-less deep search AI agent." All four analysis engines share the same node structure and differ only in their tools and data source. - Collaboration via shared log files. The agents do not call each other. Each writes to its own log; the ForumEngine tails those logs, extracts summaries, and republishes them to a shared
forum.log, where a moderator LLM periodically posts synthesis. This file-as-message-bus design is the system's defining (and most unusual) pattern. - Provider-agnostic. Every LLM interaction uses the OpenAI request format. Each of seven logical "slots" can point at a different provider via
api_key/base_url/model. - Configuration as a single Pydantic settings object loaded from one root
.env.
| Attribute | Value |
|---|---|
| Language | Python 3.9+ (3.11 recommended) |
| Web framework | Flask 2.3 + Flask-SocketIO 5.3 (eventlet) |
| Sub-app UI | Streamlit 1.28 |
| LLM interface | openai SDK, OpenAI-compatible |
| Crawler | Playwright + MediaCrawler (git submodule) |
| Database | PostgreSQL (recommended) or MySQL, via SQLAlchemy (async drivers) |
| Config | pydantic-settings ← root .env |
| Report rendering | WeasyPrint (PDF), inlined Chart.js/MathJax (HTML) |
| License | GPL-2.0 |
| Version | v1.2.1 |
2. System Architecture
2.1 Process & component topology
┌─────────────────────────────────────────────┐
│ app.py — Flask + Socket.IO (port 5000) │
│ • supervises sub-apps • /api/* control │
│ • /api/report/* (ReportEngine blueprint) │
│ • config editor → writes root .env │
└───────┬───────────────┬──────────────┬───────┘
launches as │ │ │ mounts
subprocesses ┌───────┴──────┐ ┌──────┴───────┐ ┌────┴────────┐
│ QueryEngine │ │ MediaEngine │ │ InsightEngine│
│ Streamlit │ │ Streamlit │ │ Streamlit │
│ :8503 │ │ :8502 │ │ :8501 │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
writes logs/query.log │ logs/media.log│ logs/insight.log│
▼ ▼ ▼
┌──────────────────────────────────────────────┐
│ ForumEngine (background thread in app.py) │
│ • tails the 3 engine logs │
│ • extracts SummaryNode JSON │
│ • writes logs/forum.log ([INSIGHT]/[MEDIA]/ │
│ [QUERY]/[HOST]/[SYSTEM]) │
│ • every 5 speeches → ForumHost LLM ([HOST]) │
└───────────────────────┬──────────────────────┘
agents read forum.log via │
utils/forum_reader.py ◄─────────┘
▼
┌──────────────────────────────────────────────┐
│ ReportEngine (blueprint + flask_interface) │
│ template → layout → budget → chapters → IR │
│ → renderers (HTML / PDF / Markdown) │
└──────────────────────────────────────────────┘
MindSpider (standalone CLI) ──► Database ◄── InsightEngine queries (read-only)
BroadTopicExtraction → daily_topics ; DeepSentimentCrawling → platform tables
2.2 The deep-search agent node graph
All four analysis engines (QueryEngine, MediaEngine, InsightEngine, and structurally ReportEngine) implement the same package layout:
<Engine>/
├── agent.py # orchestrator — drives the node sequence
├── llms/base.py # OpenAI-compatible LLM client (per-engine key/url/model)
├── nodes/ # processing nodes (the "graph")
│ ├── base_node.py
│ ├── search_node.py
│ ├── summary_node.py
│ ├── report_structure_node.py
│ └── formatting_node.py
├── state/state.py # mutable run state (paragraphs, search history, summaries)
├── prompts/prompts.py
├── tools/ # the data-source differentiator
└── utils/ # config.py, db.py (Insight only), text_processing.py
The execution sequence (from InsightEngine/agent.py::research):
ReportStructureNode → decompose query into N paragraphs/sections
for each paragraph:
FirstSearchNode → choose a search tool + query, execute it
FirstSummaryNode → summarize results into the paragraph
repeat MAX_REFLECTIONS×: (default 3)
ReflectionNode → decide what's missing, search again
ReflectionSummaryNode → revise the paragraph
ReportFormattingNode → assemble all paragraphs into a Markdown report
Note
State mutation is explicit: each node exposes
mutate_state(input, state, paragraph_index)and returns a newState. There is no implicit global graph runtime —agent.pyis the scheduler. This is why the same node names appear in every engine but behave according to that engine's tools.
The engines differ only in tools/:
- InsightEngine →
MediaCrawlerDB(5 read-only SQL query tools) +keyword_optimizer(a Qwen middleware that expands a query into multiple optimized keywords) + KMeans clustering oversentence-transformersembeddings (paraphrase-multilingual-MiniLM-L12-v2) to sample representative results +multilingual_sentiment_analyzer(auto-scores results, 22 languages). - QueryEngine → domestic/international news + web search.
- MediaEngine → multimodal search (video/image/rich cards).
2.3 The ForumEngine collaboration mechanism
ForumEngine/monitor.py (LogMonitor) is a polling loop (1 s tick) that:
- Tails
logs/insight.log,logs/media.log,logs/query.logby tracking byte offsets. - Identifies target lines — output from
FirstSummaryNode/ReflectionSummaryNode(matched by class name, module path, or the marker text正在生成首次段落总结/正在生成反思总结). It excludesERROR-level lines and search-node output, and handles loguru's multi-line JSON by buffering until the closing brace. - Extracts the
updated_paragraph_latest_state/paragraph_latest_statefield from each summary's JSON (with a hand-writtenfix_json_stringstate machine to repair unescaped quotes). - Writes the cleaned content to
logs/forum.logtagged[INSIGHT]/[MEDIA]/[QUERY]under a write lock. - Buffers agent speeches; once the buffer reaches
host_speech_threshold = 5, it callsForumHost.generate_host_speech()(LLM) and writes the result tagged[HOST].
A forum session begins when a FirstSummaryNode first appears (the monitor clears forum.log and starts a new session) and ends on log shrink or after 7200 inactive ticks (~2 hours).
Important
The "multi-agent debate" is implemented entirely through the filesystem. There is no IPC, queue, or shared memory between agents — only log files and a poller. This is robust (a crashed agent doesn't break the others) but couples the system tightly to exact log formats; see §14.
2.4 The ReportEngine pipeline
ReportEngine converts findings into a finished document through a node pipeline plus an intermediate representation (IR):
template_selection_node → pick 1 of 6 templates (LLM-assisted)
document_layout_node → title, TOC, theme design
word_budget_node → per-chapter length plan + directives
chapter_generation_node → generate each chapter as validated JSON blocks
│
▼
core/stitcher.py → assemble chapters into a Document IR (+ anchors/metadata)
ir/schema.py + validator → block/mark schema contract + structural validation
│
▼
renderers/
html_renderer.py → interactive HTML (Chart.js, MathJax, html2canvas,
jsPDF inlined from renderers/libs/ for offline use)
pdf_renderer.py → HTML → PDF via WeasyPrint (+ pdf_layout_optimizer,
chart_to_svg, math_to_svg for vector charts)
markdown_renderer.py → Markdown export
It exposes its own Flask interface (ReportEngine/flask_interface.py) with SSE streaming, mounted by app.py under /api/report.
2.5 MindSpider crawler
A standalone CLI subsystem (MindSpider/main.py) with two stages:
- BroadTopicExtraction — collects hot news from ~13 sources (
get_today_news.py), runs LLM topic extraction (topic_extractor.py, DeepSeek by default), persists todaily_topics. - DeepSentimentCrawling — loads keywords (
keyword_manager.py), drives platform crawls (platform_crawler.py) over the vendored MediaCrawler submodule using Playwright across 7 platforms.
3. Component Reference
| Module | Path | Responsibility |
|---|---|---|
| Main app | app.py |
Flask+SocketIO supervisor; sub-app lifecycle; config editor; control API |
| Global config | config.py |
Pydantic Settings loaded from .env; reload_settings() |
| Query Agent | QueryEngine/ |
Web/news deep-search agent |
| Media Agent | MediaEngine/ |
Multimodal deep-search agent |
| Insight Agent | InsightEngine/ |
Private-DB deep-search agent (clustering, sentiment) |
| Report Agent | ReportEngine/ |
Report assembly, IR, renderers, SSE Flask interface |
| Forum | ForumEngine/ |
monitor.py (LogMonitor) + llm_host.py (ForumHost) |
| Crawler | MindSpider/ |
Topic extraction + multi-platform crawling |
| Sentiment models | SentimentAnalysisModel/ |
5 model families, each train/predict CLI |
| Single-agent apps | SingleEngineApp/ |
Streamlit front-ends for each analysis engine |
| Shared utilities | utils/ |
forum_reader.py, github_issues.py, retry_helper.py |
| Report CLI | report_engine_only.py |
Headless report build from latest agent outputs |
| Re-render CLIs | regenerate_latest_{html,md,pdf}.py |
Re-render latest report without regeneration |
| Templates | templates/index.html |
Flask web UI |
| Tests | tests/ |
ForumEngine + ReportEngine sanitization tests |
4. Requirements & Dependencies
4.1 Runtime requirements
| Requirement | Minimum | Notes |
|---|---|---|
| Python | 3.9 | 3.11 recommended |
| Database | PostgreSQL or MySQL 5.7+ | PostgreSQL recommended |
| RAM | 2 GB+ | More for local sentiment models |
| Playwright | Chromium | playwright install chromium |
| OS | Windows / Linux / macOS | Crawler login is interactive (QR) |
4.2 Dependency groups
The root requirements.txt is organized by concern. Key pins:
| Group | Notable packages |
|---|---|
| Web framework | flask==2.3.3, flask-socketio==5.3.6, streamlit==1.28.1, python-socketio==5.8.0, eventlet==0.33.3 |
| HTTP / async | requests==2.31.0, httpx==0.28.1, aiohttp>=3.8.0, aiofiles, PySocks |
| LLM | openai>=1.3.0 (DeepSeek and others via OpenAI format) |
| Search | tavily-python>=0.3.0 |
| Data | pandas>=2.0.0, numpy>=1.24.0, jieba==0.42.1, regex |
| Databases | pymysql, aiomysql, aiosqlite, asyncpg, psycopg[binary], SQLAlchemy==2.0.35, motor, redis |
| Crawler | playwright==1.45.0, beautifulsoup4, lxml, parsel, pyexecjs, xhshow, opencv-python, Pillow==9.5.0 |
| Visualization | plotly, matplotlib==3.9.0, wordcloud==1.9.3 |
weasyprint>=60.0 |
|
| ML | torch>=2.0.0 (CPU), transformers>=4.30.0, sentence-transformers, scikit-learn, xgboost |
| Tooling | typer, python-dotenv, tenacity==8.2.2, loguru, pydantic==2.5.2, pydantic-settings==2.2.1, json-repair |
| Server | fastapi==0.110.2, uvicorn==0.29.0 |
Warning
Pillowis pinned at9.5.0(and<11under MindSpider) for Streamlit 1.28.1 compatibility (Streamlit requirespillow<11). Do not bump Pillow without testing the Streamlit sub-apps.
Note
Split requirements. Each sentiment-model family and MindSpider ships its own
requirements.txt(e.g.MindSpider/requirements.txtaddsasyncmy,pyhumps;SentimentAnalysisModel/*/requirements.txtaddpeft,accelerate,bitsandbytes,datasets). The effective dependency set is the union, assembled per use case. MediaCrawler's own deps are auto-installed on first crawl.
4.3 GPU (optional)
The local sentiment models default to CPU torch. For a GPU build:
pip3 install torch torchvision --index-url https://download.pytorch.org/whl/cu126
5. Configuration Mastery
All configuration is a single Pydantic Settings object (config.py) loaded from .env. Precedence: a .env in the current working directory wins, otherwise the project-root .env. model_config sets case_sensitive=False and extra="allow".
5.1 Server & database
| Key | Type | Default | Description |
|---|---|---|---|
HOST |
str | 0.0.0.0 |
Flask bind address |
PORT |
int | 5000 |
Web UI port |
DB_DIALECT |
str | postgresql |
postgresql or mysql |
DB_HOST |
str | your_db_host |
Database host |
DB_PORT |
int | 5432 |
5432 (PG) / 3306 (MySQL) |
DB_USER |
str | your_db_user |
Username |
DB_PASSWORD |
str | your_db_password |
Password |
DB_NAME |
str | your_db_name |
Database name |
DB_CHARSET |
str | utf8mb4 |
Charset (MySQL); emoji-safe |
5.2 LLM slots
Seven slots, each with *_API_KEY, *_BASE_URL, *_MODEL_NAME:
| Slot prefix | Default model | Default base URL |
|---|---|---|
INSIGHT_ENGINE_ |
kimi-k2-0711-preview |
https://api.moonshot.cn/v1 |
MEDIA_ENGINE_ |
gemini-2.5-pro |
https://aihubmix.com/v1 |
QUERY_ENGINE_ |
deepseek-chat |
https://api.deepseek.com |
REPORT_ENGINE_ |
gemini-2.5-pro |
https://aihubmix.com/v1 |
MINDSPIDER_ |
(provider default) | (provider default) |
FORUM_HOST_ |
qwen-plus |
(provider default) |
KEYWORD_OPTIMIZER_ |
qwen-plus |
(provider default) |
Important
The Report Agent requires a strong model. Under-powered models cause
ChapterJsonParseError/ChapterContentErrorand the report-regeneration loop (issue #666). Size this slot generously.
5.3 Web-search tools
| Key | Type | Default | Description |
|---|---|---|---|
SEARCH_TOOL_TYPE |
enum | AnspireAPI |
AnspireAPI or BochaAPI |
ANSPIRE_BASE_URL |
str | https://plugin.anspire.cn/api/ntsearch/search |
Anspire endpoint |
ANSPIRE_API_KEY |
str | — | Anspire key |
BOCHA_BASE_URL |
str | https://api.bocha.cn/v1/ai-search |
Bocha endpoint |
BOCHA_WEB_SEARCH_API_KEY |
str | — | Bocha key |
TAVILY_API_KEY |
str | — | Tavily key (optional) |
5.4 Insight-engine search tuning
These tune retrieval volume and reflection depth for the Insight agent:
| Key | Default | Description |
|---|---|---|
DEFAULT_SEARCH_HOT_CONTENT_LIMIT |
100 | Hot-list items |
DEFAULT_SEARCH_TOPIC_GLOBALLY_LIMIT_PER_TABLE |
50 | Per-table global topic cap |
DEFAULT_SEARCH_TOPIC_BY_DATE_LIMIT_PER_TABLE |
100 | Per-table date-scoped cap |
DEFAULT_GET_COMMENTS_FOR_TOPIC_LIMIT |
500 | Comments per topic |
DEFAULT_SEARCH_TOPIC_ON_PLATFORM_LIMIT |
200 | Platform-scoped topic cap |
MAX_SEARCH_RESULTS_FOR_LLM |
0 | Results passed to LLM (0 = unlimited) |
MAX_HIGH_CONFIDENCE_SENTIMENT_RESULTS |
0 | High-confidence sentiment cap (0 = unlimited) |
MAX_REFLECTIONS |
3 | Reflection rounds per paragraph |
MAX_PARAGRAPHS |
6 | Max report paragraphs |
SEARCH_TIMEOUT |
240 | Per-search timeout (s) |
MAX_CONTENT_LENGTH |
500000 | Max content chars per search |
Note
The agent deliberately ignores LLM-supplied
limitparameters and substitutes these config values — this is a guardrail so a model can't request unbounded retrieval. To change retrieval volume, edit config, not prompts.
5.5 Per-engine overrides & request timeouts
Each engine also reads *_REQUEST_TIMEOUT (and a shared LLM_REQUEST_TIMEOUT) via os.getenv in its llms/base.py. Legacy per-engine utils/config.py knobs (max_reflections, max_search_results, comprehensive_search_limit, etc.) still exist but configuration has been unified to the root .env; sub-agents inherit it automatically.
5.6 Live config editing
app.py exposes a CONFIG_KEYS allow-list (the DB, LLM, and search keys). GET /api/config reads them from the live Settings; POST /api/config writes them back to .env (write_config_values) and calls reload_settings() — changes apply without a restart.
Caution
The config editor reads and writes plaintext API keys and DB credentials to
.envover an unauthenticated endpoint (see §10). Never expose port 5000 to an untrusted network.
6. Data Layer & Schema
6.1 Connectivity
- The Insight agent connects via SQLAlchemy async engine (
InsightEngine/utils/db.py), driven byDATABASE_URL/DB_*, and performs read-only queries (theMediaCrawlerDBtool wrapsSELECT-style operations only). - MindSpider writes via its own managers (
MindSpider/schema/db_manager.py,models_sa.py,models_bigdata.py).
6.2 Core tables (MindSpider)
| Table | Purpose |
|---|---|
daily_news |
Hot news collected per day (title, link, description, rank) |
daily_topics |
LLM-extracted topics + keyword lists |
topic_news_relation |
Topic↔news links with relevance scores |
crawling_tasks |
Per-platform crawl task status/progress |
| Platform tables | xhs_note, douyin_aweme, kuaishou_video, bilibili_video, weibo_note, tieba_note, zhihu_content (inherited from MediaCrawler) |
Schema DDL lives in MindSpider/schema/mindspider_tables.sql; apply/extend with python MindSpider/schema/init_database.py.
6.3 Insight agent query tools
The Insight agent's MediaCrawlerDB exposes five retrieval tools (the agent's LLM picks one per step):
| Tool | Purpose |
|---|---|
search_hot_content |
Recent hot content (by time_period) |
search_topic_globally |
Cross-table topic search |
search_topic_by_date |
Date-bounded topic search |
get_comments_for_topic |
Comment-level retrieval |
search_topic_on_platform |
Platform-scoped topic search |
Plus analyze_sentiment as a standalone scoring tool. Results pass through keyword optimization, KMeans clustering, deduplication (by URL or content prefix), and optional sentiment scoring before reaching the LLM.
6.4 Database inspection CLI
MindSpider/schema/db_manager.py offers admin commands:
python MindSpider/schema/db_manager.py --tables # list tables
python MindSpider/schema/db_manager.py --stats # data statistics
python MindSpider/schema/db_manager.py --recent 7 # last N days
python MindSpider/schema/db_manager.py --cleanup 30 --execute # purge >30d
7. HTTP API Reference
7.1 Main app (app.py, port 5000)
| Method | Path | Purpose |
|---|---|---|
| GET | / |
Web UI |
| GET | /api/status |
Sub-app process status |
| GET | /api/start/<app_name> |
Start a Streamlit sub-app |
| GET | /api/stop/<app_name> |
Stop a sub-app |
| GET | /api/output/<app_name> |
Recent sub-app output |
| GET | /api/test_log/<app_name> |
Test-log endpoint |
| GET | /api/forum/start |
Start ForumEngine monitoring |
| GET | /api/forum/stop |
Stop ForumEngine monitoring |
| GET | /api/forum/log |
Current forum log |
| POST | /api/forum/log/history |
Forum log history |
| POST | /api/search |
Submit an analysis query |
| GET | /api/config |
Read configuration (allow-listed keys) |
| POST | /api/config |
Write configuration to .env + reload |
| GET | /api/system/status |
Overall system state |
| POST | /api/system/start |
Start all components |
| POST | /api/system/shutdown |
Shut down the system |
7.2 ReportEngine (flask_interface.py, mounted at /api/report)
| Method | Path | Purpose |
|---|---|---|
| GET | /status |
Report engine status |
| POST | /generate |
Start report generation |
| GET | /progress/<task_id> |
Progress for a task |
| GET | /stream/<task_id> |
SSE event stream |
| GET | /result/<task_id> |
Final result (HTML) |
| GET | /result/<task_id>/json |
Result as JSON/IR |
| GET | /download/<task_id> |
Download report |
| POST | /cancel/<task_id> |
Cancel a task |
| GET | /templates |
List templates |
| GET | /log / POST /log/clear |
Report log read/clear |
| GET | /export/md/<task_id> |
Export Markdown |
| GET | /export/pdf/<task_id> |
Export PDF |
| POST | /export/pdf-from-ir |
Export PDF directly from an IR payload |
Note
SECRET_KEYis hard-coded inapp.pyand Socket.IO setscors_allowed_origins="*". Both are fine for local single-user use and unsafe for public exposure — see §10.
8. Deployment
8.1 Docker Compose (recommended)
docker compose up -d
The stack (docker-compose.yml + Dockerfile) runs the app and a PostgreSQL service (db, user/pass/name bettafish, port 5432). Alternative registry mirrors are provided as comments in the compose file for slow pulls.
DB_HOST=db DB_PORT=5432 DB_USER=bettafish DB_PASSWORD=bettafish DB_NAME=bettafish DB_DIALECT=postgresql
8.2 Source deployment
Deployment checklist:
- [ ] Clone with
--recurse-submodules(orgit submodule update --init --recursive) - [ ] Python 3.11 environment (conda or uv)
- [ ] (Optional) PDF system libraries for WeasyPrint
- [ ]
pip install -r requirements.txt - [ ]
playwright install chromium - [ ] Populate
.env(DB + 7 LLM slots + search keys) - [ ] Verify DB connectivity:
python MindSpider/main.py --status - [ ] Log in to each crawl platform once (QR) before unattended crawling
- [ ]
python app.py→ confirm all three sub-apps + ForumEngine start
8.3 Port map
| Port | Service |
|---|---|
| 5000 | Flask main app / web UI |
| 8501 | InsightEngine Streamlit |
| 8502 | MediaEngine Streamlit |
| 8503 | QueryEngine Streamlit |
Warning
After a stopped run, a Streamlit sub-app may keep holding its port. If startup reports a busy port, find and kill the orphaned process before relaunching.
9. Operations
9.1 Start / stop
- Start everything:
python app.py(orPOST /api/system/start).initialize_system_components()initializes the DB, stops any stale ForumEngine, launches each Streamlit sub-app (waiting up to 30 s per app for startup), then starts ForumEngine and ReportEngine. - Stop:
POST /api/system/shutdown, or terminateapp.py(anatexithandler and the supervisor clean up child processes).
9.2 Logs
loguru is used throughout. Per-engine logs live in logs/:
| File | Producer |
|---|---|
logs/insight.log |
Insight agent |
logs/media.log |
Media agent |
logs/query.log |
Query agent |
logs/forum.log |
ForumEngine (aggregated discussion) |
logs/report*.log |
Report engine |
Important
ForumEngine parses these logs by exact format. It supports both legacy
[HH:MM:SS]and loguruYYYY-MM-DD HH:mm:ss.SSS | LEVEL |formats. If you change the log format/handler, re-validatetests/test_monitor.py— a format drift silently breaks forum collaboration.
9.3 Routine maintenance
- Purge old crawl data:
python MindSpider/schema/db_manager.py --cleanup <days> --execute. - Index high-traffic query columns; consider partitioning large platform tables.
- Use a proxy pool and conservative crawl intervals to avoid rate-limiting/bans.
- Re-render reports without re-running agents via
regenerate_latest_{html,md,pdf}.py.
9.4 Output locations
| Output | Location |
|---|---|
| Web/CLI reports | final_reports/ (+ pdf/, md/) |
| Report IR | final_reports/ir/ |
| Per-agent reports | query_engine_streamlit_reports/, media_engine_streamlit_reports/, insight_engine_streamlit_reports/ |
10. Security Analysis
Caution
BettaFish is designed as a local, single-operator research tool. Several defaults are unsafe on an untrusted network. Treat the following as negative-knowledge security facts — they are intentional design points, not bugs, but they constrain how you may deploy it.
10.1 No authentication
None of the HTTP endpoints (app.py or ReportEngine) require authentication. Anyone who can reach port 5000 can submit queries, start/stop sub-apps, read and overwrite all API keys and DB credentials via /api/config, and shut the system down. Do not bind to a public interface. Keep HOST at 127.0.0.1 for anything beyond a trusted LAN, or place the app behind an authenticating reverse proxy + firewall.
10.2 Permissive CORS and a hard-coded secret
Socket.IO is configured with cors_allowed_origins="*", and app.config['SECRET_KEY'] is a hard-coded string committed to the repository. For any non-local deployment, override the secret and restrict CORS origins.
10.3 Secrets at rest
All credentials live in plaintext in .env. The /api/config endpoint reads and rewrites that file. Restrict file permissions on .env, keep it out of version control (it is, by .gitignore), and never expose the config API.
10.4 Browser automation & login state
The crawler drives real logged-in sessions via Playwright and persists session state under MediaCrawler/browser_data/. This directory contains authenticated cookies for your social accounts — treat it as a secret and exclude it from backups/images that leave your machine.
10.5 LLM output handling
The ReportEngine ingests LLM-generated JSON and renders HTML. The repository includes a dedicated sanitization test (tests/test_report_engine_sanitization.py), and the JSON parser (utils/json_parser.py) and json-repair defensively repair malformed model output. When extending renderers, preserve sanitization — rendered reports may be shared.
10.6 Legal/compliance posture
The crawler scrapes third-party platforms. The project is explicitly research/education only, GPL-2.0, and forbids commercial use and large-scale scraping. Operators are responsible for compliance with each platform's robots.txt and terms, and with local law.
11. Code Patterns & Conventions
- Pydantic settings singleton. One global
settingsobject;reload_settings()rebuilds it after.envedits. New config is a typedFieldonSettings. - Explicit node/state machine. Agents schedule nodes by hand (
agent.py); nodes mutate an explicitState. No graph-runtime magic — easy to trace, easy to extend. - OpenAI-compatible LLM clients. Every engine's
llms/base.pyvalidatesapi_key/model_nameand raises a localized error if absent (... API key is required.). Adding a provider = setting abase_url. - Graceful retry.
utils/retry_helper.pyprovideswith_graceful_retry(CONFIG, default_return=...)decorators (e.g.SEARCH_API_RETRY_CONFIG) that return safe defaults on total failure instead of crashing a run. - Defensive JSON. Both ForumEngine (
fix_json_string) and ReportEngine (json_parser.py+json-repair) repair malformed LLM JSON; typed exceptions (ChapterJsonParseError,StageOutputFormatError,JSONParseError) carry stage context. - Config-over-prompt guardrails. Retrieval limits are forced from config, ignoring model-supplied values.
- Localization. All log messages, prompts, and error strings are Chinese; identifiers and structure are English.
- Concurrency. Thread locks guard shared state (
write_lockforforum.log,system_state_lockfor lifecycle flags); the eventlet WSGIfinishis monkey-patched to swallow benign client-disconnect exceptions.
12. Testing
Tests live in tests/ and run with pytest:
pytest tests/test_monitor.py -v
python tests/run_tests.py
| Test | Covers |
|---|---|
tests/test_monitor.py |
ForumEngine log parsing: target-line detection, JSON start/end detection, single- and multi-line JSON extraction, content formatting, value filtering |
tests/test_report_engine_sanitization.py |
ReportEngine output sanitization |
tests/forum_log_test_data.py |
Fixtures for old [HH:MM:SS] and new loguru log formats |
Important
tests/README.mddocuments a known gap: the ForumEngine timestamp regexes were written for the legacy[HH:MM:SS]format and may not fully handle loguru'sYYYY-MM-DD HH:mm:ss.SSSformat. These tests exist specifically to surface and guide fixes for that mismatch. Run them after any logging change.
13. Extending the System
13.1 Add an LLM provider
Set the slot's *_BASE_URL to any OpenAI-compatible endpoint and *_MODEL_NAME to a model it serves. No code change — the openai client handles the rest.
13.2 Integrate a custom business database (Insight agent)
- Add
BUSINESS_DB_*settings toconfig.py. - Create a tool class (e.g.
InsightEngine/tools/custom_db_tool.py) wrapping your queries. - Wire it into
InsightEngine/agent.pyand expose it as a selectable search tool.
This is the documented path to "external trends + internal insights" analysis.
13.3 Add a crawl platform
Under MindSpider/DeepSentimentCrawling/MediaCrawler/media_platform/<platform>/, implement client.py, core.py, login.py, field.py, then register the platform code.
13.4 Add a news source (topic extraction)
Add an async collector method in MindSpider/BroadTopicExtraction/get_today_news.py.
13.5 Add a report template
Drop a .md/.txt file into ReportEngine/report_template/ (or upload via the web UI). The template-selection node will consider it. Six ship by default: brand-reputation, market-competition, routine-monitoring, policy/industry, social-hot-event, and crisis-PR.
13.6 Repurpose the whole system
Because tools are OpenAI-compatible and behavior is prompt-driven, swapping the agent prompts (*/prompts/prompts.py) and the Insight DB tools retargets BettaFish to a different analytical domain (e.g. financial markets) with no orchestration changes.
14. Potential Improvements & Tech Debt
- Log-format coupling. Agent collaboration depends on parsing exact log strings. A logging change can silently break the forum without raising an error. Consider a structured side-channel (a JSONL events file or an in-process queue) decoupled from human-readable logs. The known loguru-vs-legacy regex gap (
tests/README.md) is a concrete instance. - No authentication / open config API. The biggest deployment risk. An auth layer (even a single shared token) on
app.pywould make non-localhost use viable. CORS*and the hard-codedSECRET_KEYshould be configurable. - Hard-coded clustering knobs.
ENABLE_CLUSTERING,MAX_CLUSTERED_RESULTS,RESULTS_PER_CLUSTERare module constants inInsightEngine/agent.py; promote them toSettings. - Report regeneration loop. Weak Report-Agent models can loop on token limits (#666). A hard cap on chapter-retry attempts with a graceful degradation path would bound failure (
ChapterJsonParseErrorafterchapter_max_attemptsalready exists; surfacing it to the UI sooner would help). - Dual legacy config. Per-engine
utils/config.pyknobs coexist with the unified root config. Removing the legacy path would cut confusion. - Insight dedup precision. Issue #688 notes deduplication can drop distinct URL-less results (it keys on a 100-char content prefix). A stronger identity (hash + platform + author) would reduce false collisions.
- Subprocess sub-apps. Running Streamlit apps as child processes complicates lifecycle (orphaned ports) and observability. A consolidated runtime or container-per-engine would be cleaner.
15. Quick Reference
Entry points
python app.py # full system (web UI :5000)
docker compose up -d # app + PostgreSQL
python report_engine_only.py # headless report from latest agent outputs
python MindSpider/main.py --complete --test # crawl pipeline (test volume)
streamlit run SingleEngineApp/insight_engine_streamlit_app.py --server.port 8501
Key paths
| Path | What |
|---|---|
config.py / .env |
Global configuration |
app.py |
Supervisor + control API |
<Engine>/agent.py |
Agent orchestrator |
ForumEngine/monitor.py |
Forum collaboration |
ReportEngine/flask_interface.py |
Report SSE API |
MindSpider/main.py |
Crawler CLI |
MindSpider/schema/mindspider_tables.sql |
DB schema |
logs/ |
Per-engine + forum logs |
final_reports/ |
Output reports |
Config slot cheat-sheet
DB: DB_DIALECT DB_HOST DB_PORT DB_USER DB_PASSWORD DB_NAME DB_CHARSET
LLM: {INSIGHT,MEDIA,QUERY,REPORT,MINDSPIDER}_ENGINE_* + FORUM_HOST_* + KEYWORD_OPTIMIZER_*
(each: _API_KEY _BASE_URL _MODEL_NAME)
Search: SEARCH_TOOL_TYPE ANSPIRE_* BOCHA_* TAVILY_API_KEY
Server: HOST PORT
Platform codes
xhs Xiaohongshu · dy Douyin · ks Kuaishou · bili Bilibili · wb Weibo · tieba Tieba · zhihu Zhihu