weibaohui/openDeepWiki
Go server (not AIDotNet's OpenDeepWiki) where Eino agent chains with shell access plan a wiki, write each page and answer chat.
Overview
This is weibaohui/openDeepWiki, a Go project. It is not AIDotNet/OpenDeepWiki, the larger C#/.NET project reviewed on this site under the slug opendeepwiki. The two share a name and a goal, but no code, stack or authors. This one is a single Go binary (Gin, GORM, CloudWeGo’s Eino ADK) with a React and Ant Design X front end embedded in it. Prompts, logs and generated wiki titles are in Chinese.
The design is agent-first. There is no file chunking, no embedding index and no static page plan. When a repository is added, the server shallow-clones it. A two-agent chain (toc_editor then toc_checker) then explores the checkout with file and shell tools and emits a YAML list of chapters with outlines and hints. Each chapter becomes a queued task. Another chain of three agents writes, lints and fact-checks that page. Agents are YAML files that are hot-reloaded from disk. Models are API-key rows in the database with priority-ordered failover.
Around that core sit a chat assistant over WebSocket, an MCP server for coding tools, incremental updates driven by git pull and diff, document versions and ratings, PDF export, and push-sync to another instance.
Architecture
flowchart LR
UI["React UI (embedded)"] --> API["Gin /api router"]
API --> RS["RepositoryService"]
RS --> CLONE["git clone --depth 1"]
RS --> BUS["Event buses"]
BUS --> TS["TaskService"]
TS --> SCHED["Pending scheduler (10 s)"]
SCHED --> ORCH["Orchestrator (1 worker)"]
ORCH --> W["Writers: toc / default / api / db / incremental"]
W --> SEQ["Eino SequentialAgent"]
SEQ --> TOOLS["list_dir, read_file, search_files, run_terminal_command"]
SEQ --> PM["ProxyChatModel + model pool"]
W --> DB["SQLite or MySQL (GORM)"]
API --> CHAT["Chat WebSocket: chat_assistant"]
CHAT --> PM
MCP["/mcp/streamable"] --> DOCS["DocumentService keyword search"]
DOCS --> DB
| Component | Path | Role |
|---|---|---|
| Entry point | backend/cmd/server/main.go |
Wires repos, services, writers, buses, orchestrator, MCP and router |
| Writers | backend/internal/domain/writers/ |
toc, default, api, dbmodel, incremental, title_rewriter, doc_rewriter, user_request |
| Agent runtime | backend/internal/pkg/adkagents/ |
YAML loader and watcher, Manager, BuildSequentialAgent, model pool, rate limiter |
| Agent tools | backend/internal/pkg/adkagents/tools/ |
File listing, reading and search, shell, skills, read_doc |
| Agent definitions | backend/agents/*.yaml |
Instructions, tool lists, maxIterations, optional model per agent |
| Tasks | backend/internal/service/task*.go, orchestrator/ |
Task state machine, RunAfter dependencies, ants worker pool |
| Documents | backend/internal/service/document.go |
Versions, keyword search, export |
| Chat | backend/internal/handler/chat_handler.go |
WebSocket hub and streaming of agent events |
| MCP | backend/internal/mcp/server.go |
Five read-only tools over the generated docs |
| Sync and activity | backend/internal/service/sync/, activity_scheduler.go |
Push to a remote instance; scheduled incremental refresh |
How a request flows
Adding a repository and getting its wiki:
- Create.
POST /api/repositoriescallsRepositoryService.Create. It normalises HTTPS or SSH URLs, rejects duplicates by host/owner/repo, and stores apendingrow with a timestamped local path (repository.go). - Clone and plan. The repo-added event runs
CloneRepository, which startsgit clone --depth 1in a goroutine (10-minute timeout). The same handler immediately publishes aTocWriteevent (repo_event_subscriber.go, repository_clone.go, git.go). - Queue. Tasks are created as
pending. Every 10 seconds,enqueuePendingTaskspushes them into the global orchestrator once theirRunAfterdependency has succeeded (task.go). The orchestrator is anantspool started with one worker, despite a comment that says two (main.go). - TOC.
tocWriter.genDirListbuilds aSequentialAgentoftoc_editorandtoc_checkerand asks for strict YAML withdirsandanalysis_summary.parseDirListunmarshals the reply, and each dir becomes aDocWritetask with its outline and hints stored (toc.go). - Write a page.
CreateDocWriteTaskfirst inserts a placeholder document (title cut to 20 runes) (task_helper.go). When the task runs,defaultWriter.genDocumentchainsdocument_generator,markdown_checkeranddocument_checker. Its prompt holds the repo path, title, outline and hints (default.go). - Run agents.
RunAgentToLastContentiterates the Eino runner and keeps the last message. That text is the page (agent_factory.go). - Save.
executeTaskLogicwrites the content into the placeholder document. A rewrite task creates a new version and moves theIsLatestflag (task.go).
API and database-model chapters are not part of the automatic plan. They are separate buttons (/api-analyze, /db-model-analyze) that queue the apiWriter or dbModelWriter chains.
Key components
Agents as YAML
Manager.createADKAgent turns a definition into an Eino ChatModelAgent (manager.go). It picks a model: the named pool, one named key, or a dynamic proxy over all enabled keys. It resolves each tool name through ToolProvider, sets 3 retries on rate-limit errors, and appends the iteration cap to the instruction. Unknown tool names are skipped with only a log line. For example, chat_assistant.yaml lists git_log and git_show, which the provider does not implement (tools_providers.go). Every tool is rooted at the shared Data.RepoDir, not at the current repository’s folder. An agent writing about one repo can therefore read the others.
Shell access
Most writer agents get run_terminal_command. It runs sh -c <command> with a 30-second timeout. The empty allow-list means all commands are allowed, and the only guards are a working-directory check and a substring test for .. (cmd.go). The agents read untrusted repository content, so a prompt injection in a README can reach a shell on the server. Run it in a container.
Model pool
API keys are database rows (provider, base URL, model, priority, status). The provider anthropic gets Eino’s Claude client with MaxTokens: 4096. Everything else goes through the OpenAI-compatible client (model_provider.go). ProxyChatModel tries up to three models from the pool in turn and records token usage per task (proxy_model.go). The Claude client also overwrites request headers to pose as the Claude Code CLI (claude-cli user agent, claude-code-20250219 beta) (claude_header_transport.go).
Chat and MCP
The chat handler puts the repo’s metadata and a list of document titles and IDs into a system message, adds the last 20 messages, and streams the chat_assistant agent’s events over WebSocket (chat_handler.go). The agent can read_doc a generated page or read raw source with the file tools. The MCP server registers list_repositories, get_repository, search_documents, read_document and get_document_summary (server.go). Search is a case-insensitive substring scan over every latest document in memory, capped at 20 hits (document.go).
Incremental updates
The incremental writer runs git pull (unshallowing when needed), summarises the diff since the stored commit, and asks incremental_editor/incremental_checker which chapters to add or rewrite. It then queues DocWrite or content-rewrite tasks. An activity scheduler can trigger this automatically for repos in completed state (activity_scheduler.go).
Extending it
- Agents. Edit or add YAML under the agent directory (
AGENT_DIR). Defaults are extracted from the binary on first start, and a polling file watcher reloads changes. Agent versions are tracked in the database. - Skills. Agents can call
list_skillsand run scripts fromSKILL_DIRthrough the shell tool. One sample skill ships:go-backend-stack-analyzer. - Writers. Implement
domain.Writer(Name,Generate) and register it withtaskService.AddWritersinmain.go. - Models. Add rows on the API-key page. Any OpenAI-compatible endpoint works. A
modelfield in an agent’s YAML pins that agent to one key. - External tools. Point an MCP client at
/mcp/streamable./.well-known/openapi.yamldescribes the REST API.
Running it
- Build.
make buildbuilds the front end, embeds it and the agents, and produces one Go binary. Docker images use Alpine and expose 8080.make devruns the hot-reload setup. - Config.
config.yaml(orCONFIG_PATH) sets the port, the database (sqlitedefault, ormysqlviaDB_TYPE/DB_DSN) and the data and repo directories. LLM keys are not in the config file, whatever the README’s quick-start says. They are added in the UI and stored in the database. - Requirements.
giton the PATH, network access to the git host and the LLM endpoints. Agithub.tokenkey appears in the example config, but no Go code reads it, so private repos depend on the host’s own git credentials. - Security. No authentication on any route, and CORS
*with credentials (router.go), plus the shell tool above. Treat it as a single-user tool behind a firewall.
Strengths and caveats
- Strength: grounded in the code. Pages come from agents that read the actual files. The generator prompt requires a source link on every code block and forbids invented code, and a separate
document_checkerpass reviews the draft. - Strength: operational features. Task state machine, retries, stuck-task cleanup, per-task token accounting, document versions and ratings, model failover, incremental refresh and MCP. That is more operations tooling than most projects of this size have.
- Strength: configurable without recompiling. Prompts, tool sets and model choice per agent are plain YAML.
- Caveat: serial and slow. With one orchestrator worker, a repo’s chapters are written one at a time, and the generator agent alone may take up to 100 tool iterations per page.
- Caveat: plan races the clone. The TOC task is queued while the clone is still running, and nothing waits for
ready. On a slow clone, the first plan can see a partial tree or fail and need a retry. - Caveat: Chinese-only output. The TOC prompt asks for Chinese titles, and no language setting exists.
- Caveat: unsafe defaults. Unrestricted
sh -c, cross-repo file access and an open API make it unsuitable for untrusted repos or shared hosting as shipped. - Caveat: small details. The
BuildSequentialAgenterror ingenDirListis never checked, andWriteCompleteevents are published with no subscriber.
Sources: code at bf64e72, verified Q&A.
How it answers the Open-source DeepWiki questions
Each answer was drafted by a code-reading agent at commit bf64e72. Its citations were checked mechanically. Compare with the other open-source deepwiki →
How is a repository ingested and chunked?
answeredRepository ingestion starts when a user submits a Git URL via POST /api/repositories. The RepositoryService.Create() method normalizes the URL (supporting HTTPS and SSH formats via git.NormalizeRepoURL), checks for duplicates, and persists the repo with pending status. An async event (RepositoryEventAdded) triggers cloneRepository() in repository_clone.go, which uses git clone --depth 1 (shallow clone) to clone into {Data.RepoDir}/{repoName}-{timestamp}. After cloning, it records the branch, commit hash, and directory size, then transitions the repo to ready status. There is no chunking or file-level filtering — the entire repository is available at the filesystem level. Analysis is performed by AI agents that read files directly using tools (read_file, list_dir, search_files in tools_providers.go). Supported hosts: any Git host reachable via HTTP/HTTPS or SSH — the URL normalizer validates the format but does not restrict hosts. Large-repo limits are limited only by the shallow clone strategy (--depth 1); there is no explicit file-count or size cap in the code.
How is retrieval (RAG) implemented?
answeredThere is no vector embeddings model or vector store. Retrieval is entirely keyword-based. DocumentService.SearchDocuments() performs case-insensitive substring matching across document title, filename, and content fields (lines 331–401 of document.go). Results are capped at 20 with snippet generation (200 characters around the keyword match). The MCP server exposes search_documents, read_document, and get_document_summary tools that wrap these same keyword searches. In the Q&A chat system, the assistant agent receives the full repo metadata and a list of all documents (titles and DocIDs) injected into the system message. It can then call the read_doc(doc_id) tool to retrieve the complete document content when answering questions — this is agentic file reading, not RAG. There is no embedding, no top-k vector search, no similarity scoring anywhere in the codebase. The use of ModelWithMetadata and ChatModel wrappers (e.g., ProxyChatModel) shows that after an LLM receives context through the agent tools, generation happens directly via the configured provider model.
How is the wiki structure (table of contents) determined?
answeredThe wiki table of contents is determined by an LLM agent — not by statically parsing the file tree. Triggered via POST /api/repositories/:id/directory-analyze, it creates a TocWrite task that invokes the TocWriter (toc.go). The writer builds a sequential agent pipeline of two sub-agents: toc_editor (analyzes the repo and proposes topics) then toc_checker (validates and corrects the list). The agent prompt passes the local repo path and asks it to identify the project type, tech stack, and generate a structured task list. The agent outputs must be strict YAML with two fields: dirs (array of objects with title, sort_order, outline, and hint) and analysis_summary. Each generated dir creates a DocWrite sub-task. Evidence (hints) from the TOC analysis — such as file locations, aspect details, and source references — are saved to the TaskHint repo and later fed to page-generation agents as context cues. The output YAML is parsed via parseDirList(), which does YAML extraction and unmarshalling against the DirMakerGenerationResult struct.
How are individual pages generated?
answeredIndividual pages are generated by specialized writer implementations, all implementing the Writer interface with Generate(ctx, localPath, title, taskID). Each writer builds a sequential agent pipeline from 2–3 sub-agents. For example, defaultWriter uses document_generator → markdown_checker → document_checker; dbModelWriter uses db_model_explorer → document_checker → markdown_checker; apiWriter uses api_explorer → document_checker → markdown_checker. The agent receives the local repo path, the document title, an outline (from the TOC stage), and context hints. Hints are searched by domain-specific keywords: the DB model writer searches hints for "sql", "ddl", "schema", "model" etc., while the API writer searches for "api", "route", "handler", "endpoint" etc. Each writer's prompt instructs Markdown output. Diagrams (Mermaid) are rendered client-side: the frontend's MermaidRender.tsx component renders Mermaid blocks in generated Markdown, with special handling for character escaping and dark/light theme support. Tasks run independently through an event bus and orchestrator, enabling parallelism — multiple pages can be generated concurrently. Document versions are tracked via a Version integer and IsLatest flag; regenerating a page creates a new version while preserving older ones.
How are model providers configured?
answeredProvider configuration is managed through the APIKey model stored in the database. Each key has fields: provider, base_url, api_key, model, priority, status (enabled/disabled/unavailable). The EnhancedModelProviderImpl reads keys from the api_key repo to create model instances. Two provider types are supported: "anthropic" uses the claude.NewChatModel with native Claude config (supporting thinking features and the Claude Code HTTP client), while all other providers default to OpenAI-compatible via openai.NewChatModel — this means any OpenAI-compatible endpoint (local Ollama, vLLM, etc.) works by setting the appropriate base_url. Per-stage model assignment is configured per Agent YAML definition: each agent can set a model field (referencing an APIKey by name) or use_model_pool for multi-model fallback. The ProxyChatModel wraps the model with automatic retry, rate-limit detection, and failover across the priority-ordered model pool. On rate-limit errors, the RateLimiter marks the model unavailable and GetNextModel() picks the next available one from the pool. If no model name is specified, a dynamic ProxyChatModel auto-selects from all database-enabled keys.
How is interactive Q&A / chat implemented?
answeredInteractive Q&A is implemented over WebSocket. A session is created via POST /api/repositories/:id/chat/sessions, then the frontend upgrades to a WebSocket at /repositories/:id/chat/sessions/:session_id/stream. The client sends JSON {"type":"message","content":"..."} and the server streams back event types: thinking_start, content_delta (streaming text chunks), tool_call, and assistant_end. The chat agent in chat_handler.go (the runAgent method) injects repo metadata (name, URL, branch, commit, description) plus a full document list with DocIDs into the system message. The agent can call read_doc(doc_id) to retrieve full document content — this is the primary retrieval mechanism. Conversation history (last 20 messages) is sent as context. Streaming is achieved by iterating the Eino ADK runner's event stream and forwarding each content_delta to the client. Tool calls are streamed to the frontend in real-time and saved to the database. Message limit is 1000 per session. The MCP server (mcp/server.go) provides a parallel external-facing interface with search_documents, read_document, get_document_summary, list_repositories, and get_repository tools — this is how external AI tools (Claude Code, etc.) query the wiki. No deep-research mode was found in the codebase.