mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-03 15:53:56 +00:00
v0.7.2: 根目录清理 + Agent 心跳重构 + 内嵌 ONNX 模型
- 根目录清理: branding/docs/knowledge -> assets/, package/tools/deploy -> deploy/ - meta.go: Version 0.7.2, SDKCompatibleVersion 语义改为最高兼容 - Makefile: 版本回退 0.7.2 - registry.go: 系统提示词改用 meta.Version 格式化 - Agent 心跳: reorgGraph 拆分为三个独立循环(archive/merge/review),各自可配间隔 - GraphDB: 新增 sentences 表 + 关系句子溯源 + ClearSentenceID + CleanupOrphanedSentences - Knowledge: 支持词嵌入向量化器 - NLP 四阶段流水线: Parse -> Extract -> Verify -> Fuse + SentenceRef - 移除远程 HTTP 解析器(remote_parser.go) - 新增内嵌 ONNX 模型(vocab + dep_parser.onnx): +build onnxruntime: 全量 ONNX Runtime 推理 !build onnxruntime: 内嵌词表规则式降级解析器 - config: core.agent.onnx_model_path 替代 dep_parser_url
This commit is contained in:
120
assets/docs/en/ADAPTER.md
Normal file
120
assets/docs/en/ADAPTER.md
Normal file
@ -0,0 +1,120 @@
|
||||
**中文** | [English](../en/ADAPTER.md)
|
||||
|
||||
# Lua Adapter — LLM Source Adaptation Guide
|
||||
|
||||
Each LLM API source corresponds to a Lua script, responsible for request transformation (Go unified format → API format) and response transformation (API format → Go unified format).
|
||||
|
||||
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
|
||||
|
||||
## Adapter Contract
|
||||
|
||||
The Lua script must return a table containing the following fields and functions:
|
||||
|
||||
```lua
|
||||
local adapter = {}
|
||||
|
||||
-- Metadata
|
||||
adapter.name = "my_provider" -- Unique identifier, matches adapter field in config
|
||||
adapter.version = "2.0.0"
|
||||
adapter.endpoint = "/v1/chat/completions" -- API path, appended to base_url
|
||||
adapter.headers = {} -- Additional HTTP request headers
|
||||
|
||||
-- Request transformation: Go → API
|
||||
function adapter.transform_request(raw_json)
|
||||
-- raw_json: Go's CompletionRequest JSON string
|
||||
-- Returns: JSON string to send to API
|
||||
return transformed_json
|
||||
end
|
||||
|
||||
-- Response transformation: API → Go
|
||||
function adapter.transform_response(raw_json)
|
||||
-- raw_json: API's raw response JSON string
|
||||
-- Returns: Unified CompletionResponse JSON string
|
||||
-- Unified format:
|
||||
-- { content: "", finish_reason: "", token_usage: { prompt: N, completion: N, total: N }, tool_calls?: [...] }
|
||||
return unified_json
|
||||
end
|
||||
|
||||
-- Stream chunk transformation (optional)
|
||||
function adapter.transform_stream_chunk(raw_line)
|
||||
-- raw_line: JSON string after data: in SSE
|
||||
-- Returns: JSON of { content: "", done: bool }, return "" to skip this chunk
|
||||
return chunk_json
|
||||
end
|
||||
|
||||
return adapter
|
||||
```
|
||||
|
||||
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
|
||||
|
||||
## Unified CompletionRequest Format (Go → Adapter)
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "deepseek-v4-flash",
|
||||
"messages": [
|
||||
{ "role": "system", "content": "..." },
|
||||
{ "role": "user", "content": "..." },
|
||||
{ "role": "assistant", "content": "...", "tool_calls": [...] }
|
||||
],
|
||||
"temperature": 0.7,
|
||||
"max_tokens": 4096,
|
||||
"stream": false,
|
||||
"tools": [...],
|
||||
"tool_choice": "auto"
|
||||
}
|
||||
```
|
||||
|
||||
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
|
||||
|
||||
## Unified CompletionResponse Format (Adapter → Go)
|
||||
|
||||
```json
|
||||
{
|
||||
"content": "Response content",
|
||||
"finish_reason": "stop",
|
||||
"token_usage": { "prompt": 10, "completion": 20, "total": 30 },
|
||||
"tool_calls": [
|
||||
{ "id": "call_xxx", "type": "function", "name": "tool_name", "arguments": { "key": "val" } }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
|
||||
|
||||
## Lua VM Built-in Functions
|
||||
|
||||
`json.encode(table)` — Encode Lua table to JSON string
|
||||
|
||||
`json.decode(string)` — Decode JSON string to Lua table
|
||||
|
||||
`log(level, message)` — Output log (level: info/warn/error)
|
||||
|
||||
`http_get(url)` — Perform HTTP GET request, returns response body as string
|
||||
|
||||
`http_post(url, body)` — Perform HTTP POST request, returns response body as string
|
||||
|
||||
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
|
||||
|
||||
## Adapting Typical APIs
|
||||
|
||||
| API | endpoint | auth method | Format differences |
|
||||
|-----|----------|-------------|-------------------|
|
||||
| **OpenAI** | `/chat/completions` | `Authorization: Bearer <key>` | Standard OpenAI format |
|
||||
| **DeepSeek** | `/chat/completions` | `Authorization: Bearer <key>` | OpenAI compatible, forces temperature=0 |
|
||||
| **Anthropic** | `/v1/messages` | `x-api-key: <key>` | Messages API, system message separated, content as block array |
|
||||
| **Gemini** | `/v1/models/{model}:generateContent` | `?key=<key>` or Bearer | contents/parts format, role uses model instead of assistant |
|
||||
| **Mistral** | `/v1/chat/completions` | `Authorization: Bearer <key>` | OpenAI compatible |
|
||||
| **Groq** | `/openai/v1/chat/completions` | `Authorization: Bearer <key>` | OpenAI compatible |
|
||||
| **GitHub Models** | `/chat/completions` | `Authorization: Bearer <pat>` | OpenAI compatible |
|
||||
| **Ollama** | `/api/chat` | None | Different options format |
|
||||
|
||||
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
|
||||
|
||||
## Steps to Add a New Source
|
||||
|
||||
1. Create `<name>.lua` under `internal/lua/adapters/`
|
||||
2. Script defines `transform_request` and `transform_response`
|
||||
3. (Optional) Define `transform_stream_chunk` for streaming support
|
||||
4. Build verification: `go build ./cmd/homed/`
|
||||
5. Test verification: `go test ./...`
|
||||
441
assets/docs/en/ARCHITECTURE.md
Normal file
441
assets/docs/en/ARCHITECTURE.md
Normal file
@ -0,0 +1,441 @@
|
||||
**中文** | [English](../en/ARCHITECTURE.md)
|
||||
|
||||
# HomeAgent Architecture
|
||||
|
||||
## Architectural Principles
|
||||
|
||||
HomeAgent's cognitive architecture consists of three subsystems: the event loop (eventLoop), the context window (RelevanceContext), and the stage pipeline (StageHost). Together they form the orchestration framework. Within this framework, the LLM serves as a scheduled reasoning unit; cognitive continuity is maintained by the event loop, context window, and stage pipeline.
|
||||
|
||||
**The event loop (eventLoop)** is a three-way select: `a.io.InputChan()` receives external user input and dispatches to `processTextInput` / `processMediaInput`; `a.selfInputCh` receives internal system tasks (memory merges, distillation callbacks) routed through `processConsolidation` under the `_consolidation_` output channel; `a.ctx.Done()` accepts shutdown signals. A concurrently running `interceptLoop` goroutine independently reads `a.io.InputInterruptChan()` — on receiving a high-priority interrupt, it cancels the in-flight LLM HTTP request (`a.cancelLLM()`), then writes the event to `a.interceptCh`. This channel is drained non-blockingly by `drainInterrupts()` before each LLM call in `process()`, injecting interrupts as `[打断消息]` formatted entries into message history. The three interrupt delivery paths carry distinct semantics: `cancelLLM` terminates the current HTTP request, `interceptCh` injects text before the next LLM turn, and `InjectInput` triggers a new processing cycle when the event loop is idle.
|
||||
|
||||
**The stage pipeline (StageHost)** manages two registration categories: tool definitions (ToolDef) and stage handlers (StageHandler). ToolDef includes two optional memory control fields: `NoMemory bool` — when true, the tool's output is excluded from vectorization/jieba/distillation (original text preserved); and `Cleaner func(string) string` — a filter applied before the output enters the computation layer (e.g., extracting a `content` field from JSON). Neither modifies the original output; both only affect the computation layer input. `RegisterTool` rejects duplicate names, infers the owning plugin name from the tool name prefix, and maintains a `toolPlugins` mapping. `RegisterStage` appends handlers to the corresponding stage list. On stage execution (`RunStage`), **all registered handlers execute in parallel via goroutines**, sharing a single `*StageContext` protected by `sync.RWMutex`. Individual handler panics are recovered independently without affecting other handlers. Short-circuit semantics are implemented by checking `ctx.Response != nil` — any stage handler can set this value to terminate the pipeline early. `ExecuteTool` includes built-in panic recovery with stack-trace recording. `UnregisterPluginTools` removes a plugin's tool set during hot-reload.
|
||||
|
||||
**The context window (RelevanceContext)** maintains a chronologically ordered event list. `Append` applies `CleanTemplateText` to strip QQ templates and timestamp noise before computing the embedding vector using a three-branch strategy (agent events use Response, user events use Input, cold_storage uses Input+Response). `Prune` triggers when the event count exceeds `topK`: it **unconditionally protects the last 10 events from eviction** (recency bias), scores remaining candidates against the current input via CosineSimilarity, keeps `topK - 10` highest-scoring entries (floor at 0), then re-sorts chronologically. Pruned events from sources other than `agentcli` and `terminal` are archived to the Document layer via `docStore.ContextToDoc`, retaining original timestamps. Persistence uses 5-second debounced writes to a JSON file.
|
||||
|
||||
**Tool definitions are aggregated from five sources**: IOManager-registered plugin tools; StageHost-registered SDK tools; Indexer-provided memory index tools; conditionally added built-in tools (depending on non-nil state of memory/knowledge/docStore/social/pluginReg/providerManager modules — including memory operations, knowledge retrieval, document queries, social networking, plugin reloading, child-agent spawning, per-output-channel send tools, and LLM source switching); and media processing tools added based on `pendingMedia` state. `buildToolDefs()` re-aggregates all sources on each process cycle.
|
||||
|
||||
**Provider invocation follows an ordered fallback strategy**: `ProviderManager.OrderedProviders()` returns the provider list in registration order. The `process()` inner loop iterates this list attempting `Chat()` on each. HTTP 401/403 responses mark the provider as permanently unavailable; other error types also mark unavailability but with higher tolerance. If all providers fail, an error is returned to the caller. If a call is interrupted by context cancellation while the agent is still running, it is retried (only on non-consolidation paths).
|
||||
|
||||
**The memory system adopts a three-tier storage hierarchy (Context → Document → Graph), tiering data by access locality and persistence requirements**: the Context layer is a fast-volatile working window using StaticEmbedder (pretrained word embeddings with TF-IDF fallback) for semantic relevance scoring; the Document layer **shares the same StaticEmbedder vector space with Context** (the embedder is injected into the Document Store at agent startup via `docStore.SetVectorizer(embedder)`), ensuring that relevance scores during Context pruning and semantic retrieval during Document queries operate within the same vector space — TF-IDF serves only as a fallback when the embedder is unavailable; the Graph layer uses SQLite as its persistence substrate with an entities table (nodes) and a relations table (directed edges), supporting BFS traversal recall. Data migration policies govern movement across tiers: low-scoring events sink from Context to Document (vectorized using the same embedder at archival time); cold documents, after a 72-hour no-access threshold, are distilled into triples via `docToTriples` and committed to Graph. The Indexer uses dual retrieval (entity vector similarity search + jieba keyword extraction) to construct Graph query seeds, and the `MarkRecalled` mechanism prevents entities already fetched via tool calls from being re-injected into the system prompt.
|
||||
|
||||
**The separation of core domain and application domain** constrains the kernel's responsibilities to LLM orchestration, memory management, and knowledge retrieval — no direct IO operations; all external interaction is mediated through the plugin domain. This separation limits the kernel's complexity to a verifiable scope while granting the plugin domain independent evolution: plugins can be independently developed, independently released, hot-loaded, and do not directly affect the stability of the core domain.
|
||||
|
||||
## Message Processing Flow
|
||||
|
||||
### Full Pipeline
|
||||
|
||||
```
|
||||
External input (via plugin InjectInput)
|
||||
│
|
||||
▼
|
||||
eventLoop() → processTextInput()
|
||||
│
|
||||
├── on_input stage Plugins can intercept/rewrite/short-circuit
|
||||
├── Context.Append Record to context window
|
||||
├── Context.Prune Low-relevance events archived to Document
|
||||
├── buildMemoryContext() Indexer recall → GraphDB BFS traversal
|
||||
│
|
||||
├── pre_action stage Plugins can inject system messages
|
||||
│
|
||||
├── [Tool Loop] process()
|
||||
│ ├── buildSystemPrompt Persona + Memory + Knowledge + Context
|
||||
│ ├── buildToolDefs Built-in tools + Plugin tools
|
||||
│ ├── provider.Chat() LLM call
|
||||
│ ├── post_action stage Plugins see LLM output + tool list
|
||||
│ ├── Has tools?
|
||||
│ │ ├── before_toolcall Plugins can reject/modify params
|
||||
│ │ ├── executeToolCall Route to plugin/built-in
|
||||
│ │ ├── after_toolcall Plugins can modify results
|
||||
│ │ └── → back to post_action
|
||||
│ └── No tools → exit loop
|
||||
│
|
||||
├── Context.Append(response)
|
||||
├── before_output stage Plugins can modify final text
|
||||
├── emitResponse() Send via output_send
|
||||
└── after_output stage Read-only, cleanup
|
||||
```
|
||||
|
||||
Code: `internal/agent/core/process.go` — `process()` is the main tool loop
|
||||
|
||||
### 7 Stage Hooks
|
||||
|
||||
| Stage | Trigger | Plugin Capabilities |
|
||||
|-------|---------|---------------------|
|
||||
| `on_input` | Message arrives at Agent, zero processing | Blacklist/rate-limit/short-circuit reply |
|
||||
| `pre_action` | Context ready, before LLM call | Inject external data into context |
|
||||
| `post_action` | LLM returns text + tool list | Sensitive word filter/forced redirect |
|
||||
| `before_toolcall` | Before single tool execution | Audit/reject/modify params |
|
||||
| `after_toolcall` | After single tool execution | Desensitize/sort results |
|
||||
| `before_output` | Final text ready, before sending | Format adaptation |
|
||||
| `after_output` | Already sent | Statistics/logging |
|
||||
|
||||
Code: `internal/agent/core/stages.go` — `StageHost` orchestration
|
||||
|
||||
### Loop Rules
|
||||
|
||||
`post_action → [before_toolcall → execute → after_toolcall] → post_action` forms the inner loop.
|
||||
Exit conditions: LLM has no tool calls / all rejected / exceeded limit.
|
||||
|
||||
### Short-Circuit Rules
|
||||
|
||||
Setting `ctx.Response` at any stage jumps to `after_output`.
|
||||
|
||||
|
||||
## Three-Layer Memory
|
||||
|
||||
### Memory Flow
|
||||
|
||||
```
|
||||
① Context (Working Window)
|
||||
RelevanceContext — In-memory events[] + JSON persistence
|
||||
Append: Each input, CleanTemplateText → three-branch vector(textForVector)
|
||||
agent→Response, user→Input, cold_storage→Input+Response
|
||||
StaticEmbedder pretrained word embedding / TF-IDF fallback
|
||||
Prune: StaticEmbedder CosineSimilarity, keep topK + last 10
|
||||
├── Keep → timeline → chronologically sorted → system prompt
|
||||
└── Low score → Document layer archive (original timestamp)
|
||||
Save: 5s debounce write to disk
|
||||
|
||||
↓ Prune archive ↑ LLM active recall
|
||||
|
||||
② Document (File Memory)
|
||||
DocStore — JSON files + shared StaticEmbedder vector space with Context (fallback: TF-IDF InvertedIndex)
|
||||
Write: Prune archive / doc_commit / Graph snapshot (syncGraphToDocs)
|
||||
Read:
|
||||
├── Auto-inject: Query(input, top3) → similarity summary under same vector space → [Related Memory Docs] → system prompt (read-only)
|
||||
└── LLM active: doc_query → Consume(read and delete)
|
||||
→ context.Append{Timestamp: d.CreatedAt, Source: "cold_storage"} per doc
|
||||
→ Docs written to context timeline with original timestamps, deleted from docStore
|
||||
Cold: FindColdDocs(72h, ≤2 accesses) → docToTriples → Graph
|
||||
|
||||
↓ Cold doc distillation ↑ Auto recall
|
||||
|
||||
③ Graph (Graph Database)
|
||||
SQLite — entities + relations tables
|
||||
Write: memory_commit / cold doc distillation / Pipeline rule distillation / memory_merge
|
||||
Read:
|
||||
├── Auto recall: Indexer.BuildContext(input)
|
||||
│ → CleanTemplateText → vector entity search + jieba keywords → SQLite LIKE + BFS depth=2
|
||||
│ → [Memory Index] → system prompt
|
||||
└── LLM active: memory_recall / memory_merge / memory_purge / memory_edit / memory_delete_entity
|
||||
Social: person_query / set_trait / relate (wraps GraphDB)
|
||||
|
||||
④ Distillation Pipeline (30min heartbeat)
|
||||
distillContext → window > 2×maxSize → force Prune
|
||||
syncGraphToDocs → Graph snapshot to Document (cross-layer searchable)
|
||||
reorgGraph:
|
||||
Step1: indexer.Sync — rebuild entity vector index
|
||||
Step2: docStore.Reindex — rebuild document vector index
|
||||
Step3: Cold docs → docToTriples → GraphDB.Commit
|
||||
Step4: Entity similarity (Bigram Jaccard > 0.75) → consolidation → LLM decides merge
|
||||
Step5: evaluateGraphQuality → LLM decides keep/delete
|
||||
|
||||
⑤ Pipeline Rule Distiller (every heartbeat)
|
||||
distillOnce → regex match personal info:
|
||||
我叫X / 我住在X / 我喜欢X / 我X岁 / 我的工作是X
|
||||
→ triples → GraphDB.Commit
|
||||
```
|
||||
|
||||
### Vectorization: Pretrained Word Embedding + TF-IDF Fallback
|
||||
|
||||
All vectorization unified under `StaticEmbedder` (`internal/memory/static_embedder.go`):
|
||||
|
||||
**Primary Strategy — Pretrained Word Embedding (aligned 300d)**
|
||||
- Model sources: ConceptNet Numberbatch (77-language aligned) / fastText Chinese / fastText English
|
||||
- Configured via `core.agent.embedding_model_path` (comma-separated multi-model)
|
||||
- Path containing `numberbatch` → auto-download ConceptNet; `cc.zh.` → fastText Chinese; `cc.en.` → fastText English
|
||||
- Falls back to ConceptNet by default if no match
|
||||
- **Pre-processing**: `CleanTemplateText` strips QQ tool-call templates and timestamp noise
|
||||
- **Three-branch vector source**: agent→Response, user→Input, cold_storage→Input+Response
|
||||
- **TF-IDF fallback**: auto-fallback to bag-of-words TF-IDF if model download fails or not configured
|
||||
|
||||
| Location | File | Purpose | Algorithm |
|
||||
|----------|------|---------|-----------|
|
||||
| Context Prune | `context.go:155` | Trim low-relevance context events | VectorizeClean → CosineSimilarity(queryVec, evt.Vector) |
|
||||
| DocStore Query | `document.go:206` | Recall from document memory | StaticEmbedder.Vectorize (primary) / TF-IDF (fallback) → vec.Search |
|
||||
| Indexer Entity Search | `indexer.go:96+111` | Recall from Graph | vector entity search + jieba keywords → SQLite LIKE + BFS |
|
||||
| Entity Similarity Detection | `distill.go` | Detect similar entities in Graph | Bigram Jaccard (>0.75 → consolidation) |
|
||||
|
||||
### Context Layer
|
||||
|
||||
`internal/agent/core/context.go` — `RelevanceContext`
|
||||
- Maintains recent event list, writes JSON on each Append/Prune to prevent data loss
|
||||
- Pre-vectorization pipeline runs through `CleanTemplateText` to remove template noise
|
||||
- Three-branch `textForVector`: agent events → Response, user events → Input, cold_storage → Input+Response
|
||||
- Pretrained word embedding `StaticEmbedder` → CosineSimilarity, auto-fallback to TF-IDF if unavailable
|
||||
- Protects last 10 events from eviction; excess candidates are sorted by relevance and archived to document memory
|
||||
- Archived events retain original timestamps; on `doc_query` recall they re-insert into the context timeline at their original position
|
||||
|
||||
### Document Layer
|
||||
|
||||
`internal/memory/document/document.go` — `Store`
|
||||
- Consume-on-read mode: deleted after `doc_query` retrieval
|
||||
- Dual recall: shared StaticEmbedder semantic vector search + jieba keyword extraction (falls back to char-bigram TF-IDF when model is not loaded)
|
||||
|
||||
### Graph Layer
|
||||
|
||||
`internal/memory/graph.go` — `GraphDB`
|
||||
- SQLite WAL mode, two tables (driver: mattn/go-sqlite3, CGo)
|
||||
- `Commit(triples)` — UPSERT entities + INSERT relations
|
||||
- `Recall(keywords, depth)` — Keyword LIKE search + BFS traversal
|
||||
|
||||
### Memory Tools (LLM-callable)
|
||||
|
||||
| Tool | Purpose |
|
||||
|------|---------|
|
||||
| `memory_recall` | Recall from Graph |
|
||||
| `memory_commit` | Write triples to Graph |
|
||||
| `memory_merge` | Merge two entity nodes |
|
||||
| `memory_purge` | Delete entity node |
|
||||
| `memory_edit` | Edit existing entity/relation |
|
||||
| `memory_delete_entity` | Delete entity and all its relations |
|
||||
| `memory_introspect` | View memory statistics |
|
||||
| `doc_query` | Search from Document |
|
||||
| `doc_commit` | Write to Document |
|
||||
|
||||
### Other Memory Layers
|
||||
|
||||
- **Social** (`internal/memory/social/social.go`) — Persona traits and relationship network, wraps GraphDB entity types
|
||||
- **Text Memory** (`internal/memory/text/text.go`) — Raw conversation JSONL logs, rotation strategy
|
||||
- **Memory Indexer** (`internal/memory/indexer.go`) — Entity vectorization + jieba keyword extraction, auto-inject into system prompt
|
||||
|
||||
### Distillation Pipeline
|
||||
|
||||
`internal/memory/pipeline/pipeline.go`
|
||||
- 10-minute tick, 7-day retention
|
||||
- Rule-based triple extraction (name / location / likes / age / job patterns)
|
||||
- Writes to GraphDB
|
||||
|
||||
### Context Pruning
|
||||
|
||||
```
|
||||
Heartbeat 30min:
|
||||
├── distillContext() — Distill current context
|
||||
├── syncGraphToDocs() — Graph → Document sync
|
||||
└── reorgGraph()
|
||||
├── Indexer.Sync()
|
||||
├── DocStore.Reindex()
|
||||
├── Cold docs → Graph
|
||||
└── Entity conflicts → enqueueConsolidationTask()
|
||||
│
|
||||
selfInputCh → LLM decides merge/skip
|
||||
```
|
||||
|
||||
Entity conflict detection heuristic (bigram Jaccard > 0.75), routed through `selfInputCh` internal channel, LLM makes the final merge decision.
|
||||
|
||||
|
||||
## Knowledge Base
|
||||
|
||||
`internal/knowledge/knowledge.go`
|
||||
- File directory `knowledge/<name>/content.md`
|
||||
- Independent TF-IDF index, separate from memory system
|
||||
- `knowledge_search` / `knowledge_create` / `knowledge_list`
|
||||
|
||||
|
||||
## Provider & Lua Adapter Layer
|
||||
|
||||
```
|
||||
Agent
|
||||
│
|
||||
▼
|
||||
Provider Interface (Name / Chat / ChatStream)
|
||||
│
|
||||
├── OpenAIProvider — Standard OpenAI API
|
||||
├── OllamaProvider — Local Ollama
|
||||
└── LuaAdaptedProvider (primary)
|
||||
├── Serialize CompletionRequest → JSON
|
||||
├── adapter.transform_request() → API format
|
||||
├── HTTP request + adapter.headers
|
||||
├── adapter.transform_response() → unified format
|
||||
└── Deserialize
|
||||
```
|
||||
|
||||
Code: `internal/agent/api/provider.go`
|
||||
|
||||
ProviderManager manages multiple sources, fallback in registration order. Lua adapters at `internal/lua/adapters/`, each `.lua` script defines `transform_request` / `transform_response` / `transform_stream_chunk`.
|
||||
|
||||
VM built-ins: `json.encode` / `json.decode` / `log` / `http_get` / `http_post`.
|
||||
|
||||
|
||||
## Plugin System
|
||||
|
||||
### Four Loading Methods
|
||||
|
||||
| Method | Registration Mechanism | Compilation | Usage |
|
||||
|--------|----------------------|-------------|-------|
|
||||
| Built-in | `init()` → `RegisterFactory` | `internal/plugins/` compiled into kernel | webui/cli/timer/mcp etc. |
|
||||
| External `.so` | C ABI dynamic loading | `-buildmode=c-shared` + bridge | qq/files/web/memo etc. |
|
||||
| Lua script plugin | Parse `main.lua` to register tools | No compilation, hot-reload | luaplugintest/testlua etc. |
|
||||
| SKILL plugin | Parse `SKILL.md` | Markdown definition | Loaded via clawhubadapter |
|
||||
|
||||
Built-in plugin registration: `internal/plugins/all.go` blank imports → each plugin `init()` → `Registry.Load()` scans directory to match factory.
|
||||
External plugin loading: `internal/plugin/dynamic.go` → copy to SHA256 temp path (bypass `plugin.Open` path cache) → `Open` + `Lookup("NewPlugin")`.
|
||||
Lua script plugin loading: `internal/lua/` → parse `main.lua` via Lua VM, call `start()` to register tools.
|
||||
|
||||
### PluginSDK Four Channels
|
||||
|
||||
```
|
||||
Plugin ──→ Kernel
|
||||
|
||||
RegisterTool(name, fn) ──→ buildToolDefs() / executeToolCall()
|
||||
RegisterStage(stage, fn, scope...) ──→ runStage() called at corresponding phase (scope: global / own-tools-only)
|
||||
Subscribe(event, fn) ──→ Publish() notify all subscribers
|
||||
RegisterOutputChannel(name, caps, desc, handler) ──→ output_send__{name} tool generation
|
||||
```
|
||||
|
||||
`internal/sdk/` bridges external SDK interface to kernel, defines complete PluginSDK:
|
||||
|
||||
```go
|
||||
sdk.RegisterTool(name, def, handler)
|
||||
sdk.RegisterStage(stage, handler, scope...)
|
||||
sdk.Publish(event)
|
||||
sdk.InjectInput(source, channel, payload)
|
||||
sdk.InjectInterrupt(source, channel, payload)
|
||||
sdk.Memory().Recall/Commit
|
||||
sdk.Knowledge().Search/Create
|
||||
sdk.Settings().Get/Set/List
|
||||
sdk.RegisterOutputChannel("qq", sdk.CapText|sdk.CapAudio|sdk.CapImage, "QQ channel, see output_send__qq_help for details", handler)
|
||||
```
|
||||
|
||||
### Plugin Interface
|
||||
|
||||
```go
|
||||
type Plugin interface {
|
||||
Name() string
|
||||
Start(sdk *PluginSDK) error
|
||||
Stop() error
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Output Channel System
|
||||
|
||||
Each output channel generates two tools:
|
||||
|
||||
| Tool | Type | Purpose |
|
||||
|------|------|---------|
|
||||
| `output_send__{name}` | function | Accepts `payload` (content), `meta` (JSON routing metadata), `type` (enum) — routed to plugin handler |
|
||||
| `output_send__{name}_help` | function | Returns the channel's meta format and type enum documentation |
|
||||
|
||||
Capability flags:
|
||||
|
||||
| Flag | Value | Meaning |
|
||||
|------|-------|---------|
|
||||
| CapText | 1 | Plain text |
|
||||
| CapFile | 2 | File |
|
||||
| CapImage | 4 | Image |
|
||||
| CapAudio | 8 | Audio |
|
||||
| CapStructured | 16 | Structured data |
|
||||
|
||||
System prompt injection: output gate rules, multi-call support, long message splitting.
|
||||
Child agent permission: `output_send__` prefix tools are allowed.
|
||||
|
||||
|
||||
## EventAgentLLMChain Event
|
||||
|
||||
- Event type `agent_llm_chain` emitted after each LLM turn
|
||||
- Contains the full LLM response (text + tool calls + reasoning)
|
||||
- WebUI subscribes to this event via SSE for real-time display
|
||||
- Plugins can subscribe via EventSubscriber (read-only for external plugins)
|
||||
|
||||
|
||||
## Restricted External Plugin API
|
||||
|
||||
Layered architecture: internal plugins get full PluginSDK, external plugins get restricted SDK.
|
||||
|
||||
| API | Internal Plugin | External Plugin |
|
||||
|-----|-----------------|-----------------|
|
||||
| SocialAPI | Full read/write | Read-only (GetPerson / GetTrait / GetRelations / GetNetwork / ListPersons) |
|
||||
| EventSubscriber | Subscribe + Publish | Subscribe-only (no Publish capability) |
|
||||
|
||||
Extended fields:
|
||||
- Triple extensions: Confidence, SubjectType, ObjectType
|
||||
- Relation extension: Confidence
|
||||
|
||||
|
||||
## Interrupt Mechanism
|
||||
|
||||
```
|
||||
interceptLoop (goroutine)
|
||||
├── InputInterruptChan() ← Timer/message notifications
|
||||
├── (a) cancelLLM() → Cancel Provider HTTP request
|
||||
├── (b) interceptCh → process() pre-loop read [interrupt message]
|
||||
└── (c) InjectInput() → Trigger new processing when idle
|
||||
```
|
||||
|
||||
Three delivery paths:
|
||||
|
||||
| Path | Effect | Timing |
|
||||
|------|--------|--------|
|
||||
| cancelLLM | Cancel current HTTP request | On context.Canceled |
|
||||
| interceptCh | Insert `[interrupt message]` in process() | Before each LLM call |
|
||||
| InjectInput | Trigger new processing when eventLoop is idle | No ongoing request |
|
||||
|
||||
Code: `internal/agent/core/eventloop.go` — `interceptLoop` / `drainInterrupts`
|
||||
|
||||
|
||||
## Configuration System
|
||||
|
||||
`internal/config/registry.go` — ConfigRegistry
|
||||
|
||||
- SQLite storage, `config` table + `config_<plugin>` independent tables
|
||||
- Namespaces: `core.*` / `plugin.<name>.*`
|
||||
- `RegisterDefault` inserts ~80 default keys (seeds for 8 LLM sources)
|
||||
- WebUI settings page `/api/v1/settings` for read/write
|
||||
|
||||
|
||||
## Code Structure
|
||||
|
||||
```
|
||||
cmd/homed/main.go — Entry: assembles all subsystems
|
||||
cmd/waiter/main.go — CLI client (Unix socket)
|
||||
internal/
|
||||
├── agent/
|
||||
│ ├── core/ — Agent core (eventLoop/process/stages/context)
|
||||
│ │ └── plugin_health.go — Plugin health monitoring and auto-restart
|
||||
│ ├── api/ — Provider interface + LuaAdaptedProvider
|
||||
│ ├── io/ — IOManager (queue/interrupt/output)
|
||||
│ └── personal.go — Persona loading
|
||||
├── plugin/
|
||||
│ ├── registry.go — Registry + lifecycle
|
||||
│ ├── dynamic.go — .so dynamic loader
|
||||
│ └── manifest.go — plugin.json metadata
|
||||
├── plugins/ — Built-in plugin implementations
|
||||
│ ├── all.go — Blank imports
|
||||
│ ├── webui/ — HTTP server + embedded SPA
|
||||
│ ├── cli/ — Unix socket CLI
|
||||
│ ├── timer/ — Timer
|
||||
│ ├── cmd/ — Command execution
|
||||
│ ├── mcp/ — MCP protocol
|
||||
│ ├── files/ — File operations
|
||||
│ ├── clawhubadapter/ — ClawHub adapter (OC plugin/SKILL/JS/Python sidecar)
|
||||
│ ├── agentcli/ — PTY terminal
|
||||
│ ├── healthcheck/ — Health check
|
||||
│ ├── pluginmgr/ — Plugin manager
|
||||
│ └── cfgmgr/ — Config manager
|
||||
├── sdk/ — PluginSDK definitions
|
||||
│ ├── plugin.go — Plugin interface + PluginSDK
|
||||
│ ├── memory.go — MemoryAPI
|
||||
│ ├── knowledge.go — KnowledgeAPI
|
||||
│ ├── settings.go — SettingsAPI
|
||||
│ └── llm.go — LLMAPI
|
||||
├── memory/
|
||||
│ ├── graph.go — SQLite graph database
|
||||
│ ├── indexer.go — Graph → vector index
|
||||
│ ├── vector/store.go — TF-IDF vector engine
|
||||
│ ├── document/document.go — Document memory
|
||||
│ ├── text/text.go — Text logs
|
||||
│ └── pipeline/ — Distiller
|
||||
├── knowledge/knowledge.go — Knowledge base
|
||||
├── lua/
|
||||
│ ├── vm.go — Lua VM (json/log/http)
|
||||
│ └── adapters/ — 8 LLM adapter scripts
|
||||
├── config/registry.go — SQLite config center
|
||||
├── events/bus.go — Event bus
|
||||
├── tracker/ — OverlayFS change tracking
|
||||
├── supervisor/ — Daemon management
|
||||
├── skill/ — Skill plugin management
|
||||
│ └── manager.go — Skill loading/matching
|
||||
└── meta/ — Meta information
|
||||
└── meta.go — Agent metadata
|
||||
```
|
||||
83
assets/docs/en/OVERVIEW.md
Normal file
83
assets/docs/en/OVERVIEW.md
Normal file
@ -0,0 +1,83 @@
|
||||
**中文** | [English](../en/OVERVIEW.md)
|
||||
|
||||
# HomeAgent — Project Overview
|
||||
|
||||
## What Is This
|
||||
|
||||
HomeAgent is a continuously-running personal intelligent Agent framework.
|
||||
|
||||
Core architecture: a long-running kernel process (`homed`) that connects to various IO channels (QQ, Web, CLI, etc.) through a plugin system. The kernel handles LLM orchestration, memory management, and knowledge retrieval; plugins handle all external IO — sending/receiving messages, file operations, web search, etc.
|
||||
|
||||
### Design Highlights
|
||||
|
||||
**Separation of Core Domain and Application Domain** — The kernel (core domain) performs no IO operations; all IO capabilities belong to plugins (application domain). The boundary is defined through PluginSDK:
|
||||
- Plugins register tools (Tool) with the kernel for LLM invocation
|
||||
- Plugins hook into the processing pipeline (Stage) to intercept/rewrite message flow at various phases
|
||||
- Plugins subscribe/publish events (Event) for loosely-coupled communication
|
||||
- Plugins queue or interrupt input delivery through IO API
|
||||
|
||||
The significance lies in clear responsibility boundaries: the kernel focuses on orchestration and memory management, while plugins handle IO implementation — the two are not coupled.
|
||||
|
||||
**Three-Layer Memory Architecture** — Manages information retention across long agent runtimes through a tiered storage strategy:
|
||||
- **Context Layer**: In-memory local word embedding scored event window (jieba + TF-IDF + PMI → CosineSimilarity), maintains recent context in real-time, low-relevance events automatically sink to the next layer
|
||||
- **Document Layer**: JSON files + TF-IDF vector-indexed temporary memory, supports explicit submission and implicit archival, cold data distills to Graph
|
||||
- **Graph Layer**: SQLite graph database, persists entities and relations, BFS traversal recall, distillation pipeline extracts triples from conversations
|
||||
|
||||
Three progressive layers — context, cold archive, long-term graph memory — form an information decay and consolidation pipeline from short-term to persistent storage.
|
||||
|
||||
## What It Actually Does
|
||||
|
||||
Code is in the project root, implemented in Go.
|
||||
|
||||
**Kernel** (`internal/agent/core/`):
|
||||
- `eventloop.go` — Message loop (`eventLoop`), queuing input from the IO layer
|
||||
- `process.go` / `stages.go` — Processing pipeline: memory recall → persona injection → LLM call → tool execution → output delivery, 7 stage hooks
|
||||
- `toolcall.go` — Tool scheduling and execution
|
||||
- `context.go` — Context management (pretrained word embedding scoring StaticEmbedder → CosineSimilarity, TF-IDF fallback), automatic pruning of low-relevance events
|
||||
- LLM calls abstracted through Provider interface, supports 8 LLM sources with automatic fallback
|
||||
|
||||
**Memory System** (`internal/memory/`):
|
||||
- **GraphDB** (`graph.go`) — SQLite, entities + relations tables, BFS traversal
|
||||
- **Document Store** (`document/document.go`) — Temporary memory, JSON files + TF-IDF vector index, consume-on-read
|
||||
- **Text Memory** (`text/text.go`) — Raw conversation logs, JSONL file rotation
|
||||
- **Social Store** (`social/social.go`) — Persona traits + relationship network, wraps GraphDB
|
||||
- **Memory Indexer** (`indexer.go`) — Auto-vectorizes GraphDB entities, recalls and injects into system prompt on user input
|
||||
|
||||
**Knowledge Base** (`internal/knowledge/knowledge.go`):
|
||||
- File system directory `knowledge/<name>/content.md`
|
||||
- TF-IDF vector search, independent index instance from the memory system
|
||||
- LLM operates via three tools: `knowledge_search` / `knowledge_create` / `knowledge_list`
|
||||
|
||||
**Plugin System** (`internal/plugin/`):
|
||||
- Built-in plugins: Go `init()` self-registration, compiled into kernel
|
||||
- External plugins: Go `-buildmode=c-shared` compiled to `.so`, dynamically loaded via C ABI bridge; also supports Lua script plugins
|
||||
- PluginSDK (`internal/sdk/`) defines four channels: RegisterTool / RegisterStage / Subscribe / RegisterOutputChannel
|
||||
- 7 stage hooks: on_input → pre_action → post_action → before_toolcall → after_toolcall → before_output → after_output
|
||||
|
||||
**LLM Provider** (`internal/agent/api/provider.go`):
|
||||
- Provider interface: Name / Chat / ChatStream
|
||||
- Three implementations: OpenAIProvider (standard OpenAI API), OllamaProvider (local), LuaAdaptedProvider (Lua adapter)
|
||||
- LuaAdapter located at `internal/lua/adapters/`, each LLM source has a corresponding `.lua` script
|
||||
- 8 built-in adapters: deepseek / openai / anthropic / gemini / mistral / groq / github / ollama
|
||||
|
||||
**WebUI** (`internal/plugins/webui/`):
|
||||
- Embedded SPA dashboard (`dashboard.html` packaged via `//go:embed`)
|
||||
- REST API: status query, configuration management, memory operations, knowledge management, plugin management
|
||||
- OpenAI API-compatible `/v1/chat/completions` endpoint
|
||||
- SSE event stream `/api/v1/chat/events`
|
||||
|
||||
**ClawHub Adapter** (`internal/plugins/clawhubadapter/`):
|
||||
- Unified loader for OC plugins (Node.js), Python sidecar, JS sidecar, and SKILL plugins
|
||||
- RegistryDispatcher pattern: routes registration notifications to Tool/Provider/Channel/Stage registries
|
||||
- ClawHub marketplace search and install: `clawhubadapter_search` / `clawhubadapter_npm_install`
|
||||
- 9 provider types mapped to LLM-accessible tools (image generation, web search, speech, etc.)
|
||||
- OC channels auto-registered as IO devices with text/file/image/audio capability flags
|
||||
|
||||
|
||||
## Project Status
|
||||
|
||||
Core functionality is operational. Plugin system and SDK are ready for independent external plugin development.
|
||||
|
||||
- Built-in plugins: webui / cli / timer / cmd / mcp / agentcli / healthcheck / pluginmgr / clawhubadapter / files / cfgmgr
|
||||
- External plugin examples ([homeagent-sdk](https://gitcode.com/JianFeeeee/homeagent-sdk) repo `example/`, both Go and Lua types): qq / files / web / memo / bili / editdoc / a2a / ocr / sanitizer / luaplugintest / testlua
|
||||
- Distribution: `.hmap` plugin package format, installable via WebUI
|
||||
634
assets/docs/en/PLUGIN_DEV.md
Normal file
634
assets/docs/en/PLUGIN_DEV.md
Normal file
@ -0,0 +1,634 @@
|
||||
**中文** | [English](../en/PLUGIN_DEV.md)
|
||||
|
||||
# HomeAgent Plugin Development Guide
|
||||
|
||||
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
|
||||
|
||||
## Overview
|
||||
|
||||
All external interaction capabilities of HomeAgent comes from plugins. Plugins interact with the kernel through `PluginSDK` (Go API).
|
||||
|
||||
**SDK Repository**: Plugin development tools, template code, and example plugins are hosted in the [homeagent-sdk](https://gitcode.com/JianFeeeee/homeagent-sdk) repository.
|
||||
|
||||
```bash
|
||||
git clone https://gitcode.com/JianFeeeee/homeagent-sdk.git
|
||||
cd homeagent-sdk
|
||||
```
|
||||
|
||||
Each plugin implements a three-method interface:
|
||||
|
||||
```go
|
||||
type Plugin interface {
|
||||
Name() string
|
||||
Start(sdk *PluginSDK) error
|
||||
Stop() error
|
||||
}
|
||||
```
|
||||
|
||||
### Three Development Methods
|
||||
|
||||
| Method | Use Case | Complexity |
|
||||
|--------|----------|------------|
|
||||
| **Dynamic .so/.dll plugin (recommended)** | Independently distributed third-party plugins | Medium, generated using `plugindev` toolchain |
|
||||
| **Built-in plugin** | Released with HomeAgent | Simple, requires merging into main repo |
|
||||
| **Lua script plugin** | Lightweight rapid prototyping | Simple, generated using `plugindev init --lua` |
|
||||
|
||||
---
|
||||
|
||||
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
|
||||
|
||||
## 1. Quick Start: Using the plugindev Toolchain
|
||||
|
||||
`plugindev` is the unified plugin development toolchain provided in the SDK repository, supporting both Go and Lua plugin types.
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
cd homeagent-sdk/tools/plugindev
|
||||
go build -o plugindev
|
||||
# Add plugindev to PATH or use directly
|
||||
```
|
||||
|
||||
### SDK Version Management
|
||||
|
||||
`plugindev sdk` manages local SDK versions:
|
||||
|
||||
```bash
|
||||
plugindev sdk list # list installed SDK versions
|
||||
plugindev sdk current # show current SDK version
|
||||
plugindev sdk latest # show latest available version
|
||||
plugindev sdk install v0.7.1 # install a specific version
|
||||
plugindev sdk use v0.7.1 # switch to a version
|
||||
plugindev sdk path # show current SDK path
|
||||
```
|
||||
|
||||
SDK is stored at `~/.homeagent/plugindev/sdk/<version>/`; `plugindev init` reads the current SDK version for `go.mod`.
|
||||
|
||||
### Creating a Go Plugin
|
||||
|
||||
```bash
|
||||
plugindev init myplugin
|
||||
cd myplugin
|
||||
# Edit plugin code
|
||||
vim plugin.go
|
||||
# Build and package
|
||||
plugindev build
|
||||
# Output: dist/myplugin_linux_amd64.hmap (or windows_amd64)
|
||||
```
|
||||
|
||||
### Creating a Lua Plugin
|
||||
|
||||
```bash
|
||||
plugindev init myluaplugin --lua
|
||||
cd myluaplugin
|
||||
# Edit plugin code
|
||||
vim main.lua
|
||||
# Local test
|
||||
lua main.lua
|
||||
# Build and package
|
||||
plugindev build
|
||||
# Output: dist/myluaplugin_lua.hmap
|
||||
```
|
||||
|
||||
### Template Project Structure
|
||||
|
||||
**Go plugin**:
|
||||
|
||||
```
|
||||
myplugin/
|
||||
├── plg.json — Plugin metadata (name, version, entry, target platforms)
|
||||
├── plugin.go — Plugin implementation (Plugin interface + NewPlugin export)
|
||||
├── go.mod — Go module definition
|
||||
├── README.md — Documentation
|
||||
└── thirdpart/ — Optional external source code directory
|
||||
```
|
||||
|
||||
C ABI bridge files (`z_bridge_gen.go` + `z_entry.c`) are auto-generated at build time.
|
||||
|
||||
**Lua plugin**:
|
||||
|
||||
```
|
||||
myluaplugin/
|
||||
├── plg.json — Plugin metadata (entry: "main.lua", targets: "lua")
|
||||
├── main.lua — Plugin implementation (Lua version of Plugin interface)
|
||||
├── sdk.lua — SDK mock layer (supports `lua main.lua` standalone testing)
|
||||
└── README.md — Documentation
|
||||
```
|
||||
|
||||
### Build & Package
|
||||
|
||||
`plugindev build` automatically handles compilation and packaging:
|
||||
|
||||
```bash
|
||||
cd myplugin
|
||||
plugindev build
|
||||
```
|
||||
|
||||
Execution process:
|
||||
1. Reads `plg.json` `targets` field to determine target platforms
|
||||
2. Auto-generates C ABI bridge code (`z_bridge_gen.go` + `z_entry.c`)
|
||||
3. **Go plugin**: Runs `go build -buildmode=c-shared` (produces `.so` / `.dylib` / `.dll`)
|
||||
4. **Lua plugin**: Packages source code directly, no compilation needed
|
||||
5. Generates `plugin.json` output manifest
|
||||
6. Packages as `.hmap` distribution (zip format, containing `plugin.json` + binary)
|
||||
|
||||
### plg.json (project config) vs plugin.json (output manifest)
|
||||
|
||||
| File | Purpose | Key fields |
|
||||
|------|---------|------------|
|
||||
| `plg.json` | Project metadata, maintained by developer | `targets` — build targets (e.g. `"linux/amd64,windows/amd64"`) |
|
||||
| `plugin.json` | Build artifact manifest, auto-generated | `entry` — entry filename; `platforms` — declared platforms |
|
||||
|
||||
Each target produces a separate `.hmap`; binary name by platform:
|
||||
|
||||
| Platform | Binary |
|
||||
|----------|--------|
|
||||
| Linux | `plugin.so` |
|
||||
| macOS | `plugin.dylib` |
|
||||
| Windows | `plugin.dll` |
|
||||
|
||||
### Multi-platform bundle: --bundle
|
||||
|
||||
```bash
|
||||
plugindev build --bundle
|
||||
```
|
||||
|
||||
Builds linux/amd64 + darwin/amd64 + windows/amd64 in one pass, producing a single `.hmap`
|
||||
with all platform binaries. The output manifest includes a `platforms` field.
|
||||
The kernel auto-selects the correct binary during installation.
|
||||
|
||||
Output in `dist/` directory:
|
||||
```
|
||||
dist/
|
||||
├── myplugin_linux_amd64.hmap # Single platform: Linux
|
||||
├── myplugin_windows_amd64.hmap # Single platform: Windows
|
||||
├── myplugin_darwin_amd64.hmap # Single platform: macOS
|
||||
├── myplugin_bundle.hmap # Multi-platform bundle
|
||||
└── myplugin_lua.hmap # Lua plugin
|
||||
```
|
||||
|
||||
### Deployment
|
||||
|
||||
Install via PluginMgr HTTP API (three methods):
|
||||
|
||||
```bash
|
||||
# 1. Install from URL (auto-cleanup)
|
||||
curl -X POST http://127.0.0.1:9876/plugins \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"url": "https://example.com/myplugin.hmap"}'
|
||||
|
||||
# 2. Install from local path (keeps source file)
|
||||
curl -X POST http://127.0.0.1:9876/plugins \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"path": "/path/to/myplugin.hmap"}'
|
||||
|
||||
# 3. Upload binary directly
|
||||
curl -X POST http://127.0.0.1:9876/plugins \
|
||||
--data-binary @dist/myplugin.hmap
|
||||
```
|
||||
|
||||
Reload plugins via `/api/v1/plugins/reload` or restart the kernel to activate.
|
||||
|
||||
Or upload via WebUI plugin management page.
|
||||
|
||||
---
|
||||
|
||||
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
|
||||
|
||||
## 2. Go Plugin Development in Detail
|
||||
|
||||
### Plugin Interface
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
|
||||
|
||||
type Plugin struct {
|
||||
name string
|
||||
sdk *sdk.PluginSDK
|
||||
}
|
||||
|
||||
func (p *Plugin) Name() string { return p.name }
|
||||
|
||||
func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
p.sdk = s
|
||||
// Register config items, tools, stage hooks, etc.
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Plugin) Stop() error {
|
||||
// Clean up resources
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewPluginFactory creates plugin instance (called by main.go or Windows bridge)
|
||||
func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||
return &Plugin{name: name}, nil
|
||||
}
|
||||
```
|
||||
|
||||
### Entry Point
|
||||
|
||||
`plugindev init` generates `plugin.go` with the `NewPlugin` export function directly,
|
||||
which is the entry point when the kernel loads the plugin:
|
||||
|
||||
```go
|
||||
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||
return &Plugin{name: name}, nil
|
||||
}
|
||||
```
|
||||
|
||||
At build time, `plugindev build` auto-generates C ABI bridge code (`z_bridge_gen.go` + `z_entry.c`),
|
||||
shared by both Windows DLL and Linux/macOS .so builds. No manual bridge code needed.
|
||||
|
||||
### PluginSDK Core API
|
||||
|
||||
#### Tool Registration — Make your capabilities callable by LLM
|
||||
|
||||
```go
|
||||
s.RegisterTool("weather_query", sdk.ToolDef{
|
||||
Name: "weather_query",
|
||||
Description: "Query weather for a specified city",
|
||||
NoMemory: false, // false=output participates in memory, true=skip
|
||||
// Cleaner: func(output string) string { // Optional: clean output before vector/jieba/distill
|
||||
// return extractJSON(output, "content")
|
||||
// },
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"city": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "City name, e.g. Beijing",
|
||||
},
|
||||
},
|
||||
"required": []string{"city"},
|
||||
},
|
||||
}, func(args map[string]interface{}) (interface{}, error) {
|
||||
city, _ := args["city"].(string)
|
||||
return map[string]interface{}{
|
||||
"city": city,
|
||||
"temp": 25,
|
||||
"weather": "Sunny",
|
||||
}, nil
|
||||
})
|
||||
```
|
||||
|
||||
##### NoMemory and Cleaner
|
||||
|
||||
`NoMemory` and `Cleaner` are optional fields on `ToolDef` that control how tool output participates in the **memory computation layer** (vectorization, jieba tokenization, distillation):
|
||||
|
||||
- **`NoMemory`** (default `false`): When `true`, the tool's output is excluded from all memory computation (vector, tokenization, distillation), but the original text is preserved in Context and Document. LLM attention is unaffected. Use cases: `cmd_run` (unpredictable noise in command output), pure operation tools like file upload/delete.
|
||||
|
||||
- **`Cleaner`** (optional): A function `func(output string) string`. When set, the tool output is filtered through this function before participating in vectorization/jieba/distillation. Typical use: stripping SQL prefixes, extracting a `content` field from JSON. The original output is never modified — Cleaner only affects the computation layer input.
|
||||
|
||||
Decision matrix:
|
||||
|
||||
```
|
||||
Tool output → valuable for LLM attention?
|
||||
├── No → NoMemory=true (output preserved, skipped in computation)
|
||||
└── Yes → Contains cleanable noise?
|
||||
├── Yes → Cleaner filters before computation
|
||||
└── No → Normal memory, no extra handling
|
||||
```
|
||||
|
||||
> **Note**: `Cleaner` is a Go `func` type (`json:"-"`), cannot cross C ABI boundaries. Not available for Lua plugins or remote plugins.
|
||||
|
||||
#### Stage Hooks — Intervene in message processing flow
|
||||
|
||||
7 stages:
|
||||
|
||||
| Stage | Timing | Purpose |
|
||||
|-------|--------|---------|
|
||||
| `on_input` | Message just arrived at Agent | Blacklist, rate-limit, short-circuit |
|
||||
| `pre_action` | About to call LLM | Inject context |
|
||||
| `post_action` | LLM returned results | Modify output/tool list |
|
||||
| `before_toolcall` | Before tool execution | Audit, reject, modify params |
|
||||
| `after_toolcall` | After tool execution | Desensitize, rewrite results |
|
||||
| `before_output` | Before output | Format adaptation, leak cleanup |
|
||||
| `after_output` | After output | Statistics/logging |
|
||||
|
||||
```go
|
||||
// Global: receive all stage events
|
||||
s.RegisterStage(sdk.StagePreAction, func(ctx *sdk.StageContext) error {
|
||||
ctx.Lock()
|
||||
ctx.ContextMsgs = append(ctx.ContextMsgs, map[string]interface{}{
|
||||
"role": "system",
|
||||
"content": "Injected context content",
|
||||
})
|
||||
ctx.Unlock()
|
||||
return nil
|
||||
})
|
||||
|
||||
// Own tools only: only before_toolcall/after_toolcall for this plugin's tools
|
||||
s.RegisterStage(sdk.StageBeforeToolcall, myHandler, sdk.StageScopeOwnTools)
|
||||
```
|
||||
|
||||
#### Configuration Management
|
||||
|
||||
```go
|
||||
// Register config definition
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{
|
||||
Key: "plugin.myplugin.api_key",
|
||||
Default: "",
|
||||
Type: "string",
|
||||
DisplayName: "API Key",
|
||||
Description: "API key",
|
||||
Category: "myplugin",
|
||||
})
|
||||
|
||||
// Read/write config
|
||||
val, err := s.Settings().Get("api_key")
|
||||
s.Settings().Set("api_key", "new-value")
|
||||
|
||||
// Read core config
|
||||
s.Settings().GetCore("llm.model")
|
||||
|
||||
// Read other plugin's config
|
||||
s.Settings().GetPlugin("other_plugin", "some_key")
|
||||
```
|
||||
|
||||
#### Input Delivery
|
||||
|
||||
```go
|
||||
// Normal delivery (processed in order)
|
||||
s.InjectText(source, channel, text string)
|
||||
|
||||
// Interrupt delivery (can interrupt current LLM processing)
|
||||
s.InjectInterruptText(source, channel, text string)
|
||||
|
||||
// No memory recording
|
||||
s.InjectTextNoMemory(source, channel, text string)
|
||||
```
|
||||
|
||||
#### Event Subscription
|
||||
|
||||
```go
|
||||
import "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
|
||||
|
||||
unsub := s.Events().Subscribe(sdk.EventToolCall, func(evt *sdk.Event) {
|
||||
log.Printf("Tool was called: %v", evt.Payload)
|
||||
})
|
||||
defer unsub()
|
||||
```
|
||||
|
||||
#### Capability Access
|
||||
|
||||
```go
|
||||
// Graph Memory (entity-relation store)
|
||||
entities, relations, err := s.Memory().Recall([]string{"keyword"}, 2)
|
||||
|
||||
// Document Memory (vector store)
|
||||
docs := s.DocMemory().Query("query text", 3)
|
||||
|
||||
// Knowledge
|
||||
results, err := s.Knowledge().Search("query", 5)
|
||||
|
||||
// LLM source management
|
||||
s.LLM().ListSources() // returns []string
|
||||
s.LLM().SetSource("deepseek")
|
||||
```
|
||||
|
||||
#### Event Subscription (built-in plugins)
|
||||
|
||||
```go
|
||||
// Subscribe to system events, returns unsubscribe function
|
||||
unsub := s.Subscribe("tool_call", func(evt *events.Event) {
|
||||
log.Printf("Tool was called: %v", evt.Payload)
|
||||
})
|
||||
defer unsub()
|
||||
|
||||
// Publish event
|
||||
s.Publish(&events.Event{
|
||||
Type: "custom_event",
|
||||
Payload: map[string]interface{}{"key": "value"},
|
||||
})
|
||||
```
|
||||
|
||||
#### IO Channel Management (built-in plugins)
|
||||
|
||||
```go
|
||||
// Register a channel (bind device driver), dev must implement the agentIO.Device interface:
|
||||
// Name() string
|
||||
// Type() DeviceType
|
||||
// Description() string
|
||||
// Tools() []ToolDef
|
||||
// Execute(tool string, args map[string]interface{}) (interface{}, error)
|
||||
// Start() error
|
||||
// Stop() error
|
||||
// OutputCapabilities() OutputCapability
|
||||
s.RegisterChannel("mydevice", deviceImpl)
|
||||
|
||||
// Unregister a channel
|
||||
s.UnregisterChannel("mydevice")
|
||||
|
||||
// List all channels
|
||||
channels := s.ListChannels()
|
||||
```
|
||||
|
||||
#### Input Delivery (built-in plugins)
|
||||
|
||||
```go
|
||||
// Queued delivery (processed in order)
|
||||
s.InjectInput(source, channel, eventType string, payload map[string]interface{})
|
||||
|
||||
// Synchronous delivery (waits for response)
|
||||
resp := s.InjectInputSync(source, channel, eventType string, payload map[string]interface{})
|
||||
|
||||
// Interrupt delivery (can preempt current LLM processing)
|
||||
s.InjectInterrupt(source, channel, eventType string, payload map[string]interface{})
|
||||
|
||||
// Synchronous text shortcuts
|
||||
resp := s.InjectTextSync(source, channel, text string)
|
||||
resp := s.InjectTextSyncNoMemory(source, channel, text string)
|
||||
|
||||
// Get output channel
|
||||
outputCh := s.OutputChan()
|
||||
```
|
||||
|
||||
> **Note**: `Subscribe`, `Publish`, `RegisterChannel`, `UnregisterChannel`, `ListChannels`, `InjectInput`, `InjectInputSync`, `InjectInterrupt`, `InjectTextSync`, `InjectTextSyncNoMemory`, `OutputChan` are only available in built-in plugins (`internal/sdk` package). External dynamic plugins should use the public APIs: `InjectText`, `InjectInterruptText`, `InjectTextNoMemory`.
|
||||
|
||||
---
|
||||
|
||||
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
|
||||
|
||||
## 3. Lua Plugin Development in Detail
|
||||
|
||||
Lua plugins are suitable for lightweight rapid prototyping, requiring no Go compilation environment. Changes take effect after kernel restart.
|
||||
|
||||
### Plugin Structure
|
||||
|
||||
```lua
|
||||
-- main.lua
|
||||
local plugin = {
|
||||
name = "myluaplugin"
|
||||
}
|
||||
|
||||
function plugin.start(sdk)
|
||||
sdk.log("info", "myluaplugin starting...")
|
||||
|
||||
sdk.register_tool("myluaplugin_hello", {
|
||||
description = "A hello world tool",
|
||||
parameters = {
|
||||
type = "object",
|
||||
properties = {}
|
||||
}
|
||||
}, function(args)
|
||||
return { content = "Hello from myluaplugin plugin!" }
|
||||
end)
|
||||
|
||||
sdk.log("info", "myluaplugin started")
|
||||
end
|
||||
|
||||
function plugin.stop()
|
||||
sdk.log("info", "myluaplugin stopped")
|
||||
end
|
||||
|
||||
return plugin
|
||||
```
|
||||
|
||||
### SDK Mock Layer
|
||||
|
||||
`sdk.lua` provides a pure Lua SDK mock implementation, supporting `lua main.lua` standalone testing:
|
||||
|
||||
```bash
|
||||
lua main.lua
|
||||
# Output:
|
||||
# [lua-plugin] info: myluaplugin starting...
|
||||
# [lua-plugin] register_tool: myluaplugin_hello
|
||||
# [lua-plugin] info: myluaplugin started
|
||||
```
|
||||
|
||||
When running inside the kernel, `sdk.*` global variables are injected by the Go layer, and all functions marked with `-- !impl` are replaced with real implementations.
|
||||
|
||||
### Lua SDK API
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `sdk.log(level, msg)` | Log output |
|
||||
| `sdk.register_tool(name, def, handler)` | Register tool |
|
||||
| `sdk.register_stage(stage, handler)` | Register stage hook |
|
||||
| `sdk.register_api(name)` | Register API |
|
||||
| `sdk.get_setting(key)` | Read config |
|
||||
| `sdk.set_setting(key, value)` | Write config |
|
||||
| `sdk.inject_text(source, channel, text)` | Deliver text message |
|
||||
| `sdk.inject_interrupt(source, channel, text)` | Interrupt delivery |
|
||||
| `sdk.json.encode(val)` | JSON encode |
|
||||
| `sdk.json.decode(str)` | JSON decode |
|
||||
| `sdk.http.get(url)` | HTTP GET request (`-- !impl`) |
|
||||
| `sdk.http.post(url, body, content_type)` | HTTP POST request (`-- !impl`) |
|
||||
|
||||
> **Note**: Lua plugin's `sdk.register_stage` callback currently only receives `raw_message`, `user_id`, `phase` fields. The functionality is limited. For complex stage handling logic, use Go plugins.
|
||||
|
||||
---
|
||||
|
||||
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
|
||||
|
||||
## 4. Built-in Plugins
|
||||
|
||||
Built-in plugins use `init()` self-registration, compiled into the kernel, no separate deployment needed.
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
internal/plugins/yourplugin/
|
||||
plugin.go — Plugin main file
|
||||
```
|
||||
|
||||
### Minimal Plugin Example
|
||||
|
||||
```go
|
||||
package yourplugin
|
||||
|
||||
import (
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
|
||||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||
)
|
||||
|
||||
func init() {
|
||||
plugin.RegisterFactory("yourplugin", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||
return New(name), nil
|
||||
})
|
||||
}
|
||||
|
||||
type Plugin struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func New(name string) *Plugin {
|
||||
return &Plugin{name: name}
|
||||
}
|
||||
|
||||
func (p *Plugin) Name() string { return p.name }
|
||||
|
||||
func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
// Initialize plugin here: start goroutines, register tools, subscribe events, etc.
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Plugin) Stop() error {
|
||||
// Clean up resources
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### Register with Kernel
|
||||
|
||||
Add blank import in `internal/plugins/all.go`:
|
||||
|
||||
```go
|
||||
package plugins
|
||||
|
||||
import (
|
||||
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/yourplugin"
|
||||
// ... other plugins
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
|
||||
|
||||
## 5. Best Practices
|
||||
|
||||
1. `Start()` is non-blocking — start long tasks in goroutines, don't block Start
|
||||
2. `Stop()` cleans up resources — close connections, stop goroutines, cancel subscriptions
|
||||
3. Unique tool names — use plugin name prefix to avoid conflicts
|
||||
4. When handler returns `error`, LLM will receive it and may retry
|
||||
5. Use `InjectInterruptText` for interrupts, `InjectText` for normal delivery
|
||||
6. Use `Settings().Get/Set` for config, don't hardcode
|
||||
7. External Go plugins compile independently, not tied to kernel version; only built-in plugins need recompilation with kernel
|
||||
|
||||
---
|
||||
|
||||
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
|
||||
|
||||
## 6. Example Plugin Reference
|
||||
|
||||
### SDK Repository Examples (`homeagent-sdk/example/`)
|
||||
|
||||
| Example | Type | Features |
|
||||
|---------|------|----------|
|
||||
| [memo](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/memo) | Go | Memo management, PreAction injection + timed interrupt dual reminder |
|
||||
| [files](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/files) | Go | File system operations, 4 write modes, sandbox isolation |
|
||||
| [browser](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/browser) | Go | Web search + HTTP fetch (SSRF) + Chromium render (merged from web/webfetch) |
|
||||
| [bili](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/bili) | Go | Bilibili video download (yt-dlp) |
|
||||
| [qq](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/qq) | Go | NapCat OneBot integration, 17 tools |
|
||||
| [editdoc](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/editdoc) | Go | Office document editing and format conversion |
|
||||
| [a2a](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/a2a) | Go | Agent-to-Agent protocol |
|
||||
| [ocr](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/ocr) | Go | Offline text recognition (Tesseract) |
|
||||
| [sanitizer](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/sanitizer) | Go | Output sanitizer filter |
|
||||
|
||||
### Built-in Plugins
|
||||
|
||||
| Plugin | Location | Features |
|
||||
|--------|----------|----------|
|
||||
| Timer | `internal/plugins/timer/` | Simplest complete example, registers one tool + interrupt feedback |
|
||||
| CLI | `internal/plugins/cli/` | Unix socket listener + synchronous request-response |
|
||||
| WebUI | `internal/plugins/webui/` | HTTP service + dependency injection |
|
||||
|
||||
---
|
||||
|
||||
*Want to understand the project goals? See [OVERVIEW.md](OVERVIEW.md).*
|
||||
*Want to understand the architecture? See [ARCHITECTURE.md](ARCHITECTURE.md).*
|
||||
*SDK repository and development tools? See [homeagent-sdk](https://gitcode.com/JianFeeeee/homeagent-sdk).*
|
||||
Reference in New Issue
Block a user