mirror of
https://github.com/larchanka/manbot.git
synced 2026-05-13 21:42:08 +00:00
Release - v2.0.0
This commit is contained in:
@@ -91,7 +91,7 @@ AI-Agent/
|
||||
### Agent layer
|
||||
|
||||
- **Planner**: Listens for `plan.create`; uses Lemonade + Model Router to produce a DAG; validates with `validateGraph`; responds with plan.
|
||||
- **Executor**: Listens for `plan.execute`; computes ready nodes (parallel batch, concurrency limit); dispatches `node.execute` to `node.service` (model-router, rag-service, critic-agent, tool-host); waits for response by `correlationId`; updates Task Memory; after DAG, optional reflection loop (Critic → REVISE → re-run generation, max 3); aggregates result and completes task.
|
||||
- **Executor**: Listens for `plan.execute`; traverses the DAG and manages **Autonomous Agent Loops**; provides an agent system prompt and core tools; handles **Dynamic Skill Loading**; after DAG, optional reflection loop.
|
||||
- **Critic**: Listens for `reflection.evaluate`; uses Lemonade with Critic prompt; returns structured `{ decision: PASS|REVISE, feedback, score }`.
|
||||
|
||||
### Service layer
|
||||
@@ -121,7 +121,7 @@ AI-Agent/
|
||||
3. Core → Planner: `plan.create` (goal); Planner → Core: plan (DAG).
|
||||
4. Core → Task Memory: `task.create` (taskId, goal, nodes, edges).
|
||||
5. Core → Executor: `plan.execute` (taskId, plan, goal).
|
||||
6. Executor runs DAG: `node.execute` to model-router, rag-service, critic-agent, tool-host; Task Memory updates; optional Critic revision loop.
|
||||
6. Executor runs DAG: executes specialized **Agents** with instructions; Agents use tools and dynamically call `load_skill`; Task Memory updates; optional Critic revision loop.
|
||||
7. Executor → Core: response with aggregated result.
|
||||
8. Core → Telegram Adapter: `telegram.send` (chatId, text).
|
||||
9. User sees reply in Telegram.
|
||||
|
||||
@@ -13,7 +13,7 @@ A multi-process AI platform with type-safe IPC and capability-graph execution. U
|
||||
## Features
|
||||
|
||||
- **Multi-agent pipeline**: Planner → Task Memory → Executor → Critic (optional revision loop)
|
||||
- **Capability graph (DAG)**: Nodes for `generate_text`, `semantic_search`, `reflect`, `tool`; parallel execution where dependencies allow
|
||||
- **Capability graph (DAG)**: Nodes for specialized **Agents** and tool-agnostic LLM generation; parallel execution where dependencies allow
|
||||
- **Type-safe IPC**: JSONL over stdin/stdout with Zod-validated envelopes
|
||||
- **Conversation Memory**: Short-term memory (last 5 tasks) is injected into the Planner for immediate session context; `/new` resets the session and archives the conversation.
|
||||
- **Session-Scoped RAG**: Memory searches are session-scoped by default to prevent context leakage after `/new`, with an optional `global` scope.
|
||||
|
||||
@@ -47,13 +47,14 @@ Prevents chaotic reasoning loops.
|
||||
|
||||
## 4. Capability Graph Pattern
|
||||
|
||||
Planner produces a Directed Acyclic Graph (DAG):
|
||||
Planner produces a Directed Acyclic Graph (DAG) consisting of independent **Agents**:
|
||||
|
||||
Example:
|
||||
|
||||
semantic_search → sql_query → generate_text → reflect
|
||||
research_agent → coding_agent → testing_agent → analysis_agent
|
||||
|
||||
Executor processes nodes sequentially or parallel when possible.
|
||||
Nodes are now specialized autonomous agents that can dynamically load instructions (Skills) as needed.
|
||||
Executor processes these agent nodes sequentially or parallel when possible.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,43 +1,41 @@
|
||||
# CAPABILITY GRAPH JSON FORMAT
|
||||
|
||||
## Execution Plan Structure
|
||||
|
||||
```
|
||||
```json
|
||||
{
|
||||
"taskId": "uuid",
|
||||
"complexity": "medium",
|
||||
"reflectionMode": "NORMAL",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node1",
|
||||
"type": "semantic_search",
|
||||
"service": "rag-service",
|
||||
"id": "research-01",
|
||||
"type": "agent",
|
||||
"service": "executor",
|
||||
"input": {
|
||||
"query": "scalable API architecture"
|
||||
"name": "Research Agent",
|
||||
"instructions": "Search for the latest F1 results using http_search."
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node2",
|
||||
"id": "summary-01",
|
||||
"type": "agent",
|
||||
"service": "executor",
|
||||
"input": {
|
||||
"name": "Summary Agent",
|
||||
"instructions": "Summarize the research from {{research-01}} and load the 'email' skill to prepare a draft."
|
||||
},
|
||||
"dependsOn": ["research-01"]
|
||||
},
|
||||
{
|
||||
"id": "node-final",
|
||||
"type": "generate_text",
|
||||
"service": "model-router",
|
||||
"input": {
|
||||
"modelClass": "medium",
|
||||
"promptTemplate": "architecture_template",
|
||||
"dependsOn": ["node1"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "node3",
|
||||
"type": "reflect",
|
||||
"service": "critic-agent",
|
||||
"input": {
|
||||
"dependsOn": ["node2"]
|
||||
"prompt": "Construct final Telegram response: {{summary-01}}",
|
||||
"system_prompt": "analyzer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{ "from": "node1", "to": "node2" },
|
||||
{ "from": "node2", "to": "node3" }
|
||||
{ "from": "research-01", "to": "summary-01" },
|
||||
{ "from": "summary-01", "to": "node-final" }
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -65,8 +63,9 @@ interface CapabilityNode {
|
||||
|
||||
## Node types (model-router / Generator)
|
||||
|
||||
- **generate_text** — LLM generation; input: `modelClass`, optional `prompt`, context from dependencies.
|
||||
- **summarize** — Memory extraction from chat history; input: `chatHistory` (text). Uses dedicated summarizer system prompt. Used by Orchestrator for conversation archiving.
|
||||
- **agent** — Autonomous LLM loop; input: `name`, `instructions`. High-level strategic node that can use tools and dynamically call `load_skill`.
|
||||
- **generate_text** — Simple LLM generation; input: `prompt`, context from dependencies. Used for final consolidation.
|
||||
- **summarize** — Memory extraction from chat history; input: `chatHistory`.
|
||||
|
||||
## Graph Rules
|
||||
|
||||
|
||||
@@ -26,8 +26,8 @@ Responsibilities:
|
||||
Responsibilities:
|
||||
- Intent analysis
|
||||
- Capability determination
|
||||
- Execution graph creation
|
||||
- Model complexity selection
|
||||
- Creates an **Agent-based Execution Graph** (DAG)
|
||||
- Assigns specialized roles to agents (input: `name`, `instructions`)
|
||||
|
||||
Input:
|
||||
- User message
|
||||
@@ -41,10 +41,10 @@ Output:
|
||||
|
||||
### 3. Executor Agent
|
||||
Responsibilities:
|
||||
- Execute DAG nodes
|
||||
- Call services
|
||||
- Aggregate intermediate results
|
||||
- Update task memory
|
||||
- Traverses the DAG and manages **Autonomous Agent Loops**
|
||||
- Provides core tools (shell, browser, search) to agents
|
||||
- Handles **Dynamic Skill Loading** via `load_skill` tool
|
||||
- Aggregates results and updates task memory
|
||||
|
||||
---
|
||||
|
||||
|
||||
1374
package-lock.json
generated
1374
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,7 @@
|
||||
"better-sqlite3": "^12.6.2",
|
||||
"ffmpeg-static": "^5.3.0",
|
||||
"node-cron": "^4.2.1",
|
||||
"node-telegram-bot-api": "^0.67.0",
|
||||
"node-telegram-bot-api": "^0.63.0",
|
||||
"nodejs-whisper": "^0.2.9",
|
||||
"pino": "^9.5.0",
|
||||
"playwright": "^1.48.0",
|
||||
@@ -43,9 +43,9 @@
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"@types/node-telegram-bot-api": "^0.64.13",
|
||||
"@types/turndown": "^5.0.5",
|
||||
"@vitest/coverage-v8": "^4.0.18",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.7.2",
|
||||
"vitest": "^4.0.18",
|
||||
"@vitest/coverage-v8": "^4.0.18"
|
||||
"vitest": "^4.0.18"
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,20 @@ const SKILL_TOOLS: any[] = [
|
||||
required: ["local_path"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "load_skill",
|
||||
description: "Load detailed instructions for a specific skill. Use this if you need more information on how to use a skill from the available list.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
skillName: { type: "string", description: "The name of the skill to load" }
|
||||
},
|
||||
required: ["skillName"]
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@@ -89,6 +103,7 @@ import { responsePayloadSchema } from "../shared/protocol.js";
|
||||
import { getConfig } from "../shared/config.js";
|
||||
import { SkillManager } from "../services/skill-manager.js";
|
||||
import { parseTimeExpression } from "../services/time-parser.js";
|
||||
import { buildAgentPrompt } from "./prompts/agent-node.js";
|
||||
|
||||
const PLAN_EXECUTE = "plan.execute";
|
||||
const NODE_EXECUTE = "node.execute";
|
||||
@@ -102,7 +117,8 @@ const TYPE_TO_SERVICE: Record<string, string> = {
|
||||
generate_text: "model-router",
|
||||
generate: "model-router",
|
||||
summarize: "model-router",
|
||||
skill: "model-router", // skills are handled by generation with custom system prompt
|
||||
skill: "model-router", // legacy skill support
|
||||
agent: "model-router", // new agent node support
|
||||
tool: "tool-host",
|
||||
semantic_search: "rag-service",
|
||||
reflect: "critic-agent",
|
||||
@@ -482,10 +498,11 @@ export class ExecutorAgent extends BaseProcess {
|
||||
...(Object.keys(context).length > 0 && { context }),
|
||||
};
|
||||
|
||||
// Handle skill nodes by swapping prompt and injecting skill system prompt
|
||||
if (node.type === "skill") {
|
||||
// Handle agent/skill nodes by running an LLM loop with tool access
|
||||
if (node.type === "agent" || node.type === "skill") {
|
||||
const isAgent = node.type === "agent";
|
||||
const skillName = (node.input?.skillName ?? node.input?.skill) as string;
|
||||
let task = (node.input?.task ?? node.input?.prompt ?? "") as string;
|
||||
let task = (node.input?.task ?? node.input?.prompt ?? node.input?.instructions ?? "") as string;
|
||||
|
||||
// Replace placeholders from context if present (e.g. {{nodeId}})
|
||||
for (const [key, value] of Object.entries(context)) {
|
||||
@@ -498,26 +515,39 @@ export class ExecutorAgent extends BaseProcess {
|
||||
}
|
||||
}
|
||||
|
||||
// Active Agent/Skill loop
|
||||
(async () => {
|
||||
try {
|
||||
const messages: any[] = [];
|
||||
|
||||
if (isAgent) {
|
||||
const skillsList = this.skillManager.listSkills();
|
||||
const skillsDescription = skillsList.map(s => `- ${s.name}: ${s.description}`).join("\n");
|
||||
const agentPrompt = buildAgentPrompt(
|
||||
(node.input?.name as string) || "Task Agent",
|
||||
task,
|
||||
skillsDescription,
|
||||
new Date().toISOString()
|
||||
);
|
||||
messages.push({ role: "system", content: agentPrompt });
|
||||
messages.push({ role: "user", content: "Proceed with the task." });
|
||||
} else {
|
||||
// Legacy skill handling
|
||||
const skillPrompt = this.skillManager.getSkillPrompt(skillName);
|
||||
if (!skillPrompt) {
|
||||
reject(new Error(`Skill prompt not found: ${skillName}`));
|
||||
return;
|
||||
}
|
||||
|
||||
// Active Skill loop
|
||||
(async () => {
|
||||
try {
|
||||
const messages: any[] = [
|
||||
{ role: "system", content: skillPrompt },
|
||||
{ role: "user", content: task }
|
||||
];
|
||||
messages.push({ role: "system", content: skillPrompt });
|
||||
messages.push({ role: "user", content: task });
|
||||
}
|
||||
|
||||
let turnCount = 0;
|
||||
while (turnCount < MAX_SKILL_TURNS) {
|
||||
const genResponse = await this.callModelRouter(taskId, node.id, {
|
||||
type: "generate_text",
|
||||
input: {
|
||||
prompt: task, // Fallback, messages are used if present
|
||||
prompt: task,
|
||||
messages,
|
||||
tools: SKILL_TOOLS
|
||||
},
|
||||
@@ -532,35 +562,41 @@ export class ExecutorAgent extends BaseProcess {
|
||||
|
||||
// Execute each tool call
|
||||
for (const tc of result.tool_calls) {
|
||||
const toolName = tc.function.name;
|
||||
let args = tc.function.arguments;
|
||||
if (typeof args === "string") {
|
||||
try {
|
||||
args = JSON.parse(args);
|
||||
} catch (e) {
|
||||
// fallback to raw string if not JSON
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
let toolResult: any;
|
||||
if (toolName === "load_skill") {
|
||||
const sn = args.skillName;
|
||||
const content = this.skillManager.getSkillPrompt(sn);
|
||||
toolResult = content ? `Skill '${sn}' loaded:\n${content}` : `Skill '${sn}' not found.`;
|
||||
} else {
|
||||
toolResult = await this.callTool(taskId, node.id, toolName, args, context);
|
||||
}
|
||||
const toolResult = await this.callTool(taskId, node.id, tc.function.name, args, context);
|
||||
|
||||
let content = typeof toolResult === "string" ? toolResult : JSON.stringify(toolResult);
|
||||
// Safety truncation for large web pages or shell outputs to stay within context limits
|
||||
if (content.length > 30000) {
|
||||
content = content.substring(0, 30000) + "\n\n...[TRUNCATED DUE TO LENGTH]...";
|
||||
content = content.substring(0, 30000) + "\n\n...[TRUNCATED]...";
|
||||
}
|
||||
messages.push({
|
||||
role: "tool",
|
||||
tool_call_id: tc.id,
|
||||
name: tc.function.name,
|
||||
name: toolName,
|
||||
content
|
||||
});
|
||||
}
|
||||
turnCount++;
|
||||
} else {
|
||||
// No more tool calls, we are done
|
||||
resolve(result);
|
||||
return;
|
||||
}
|
||||
}
|
||||
reject(new Error(`Skill exceeded maximum turns (${MAX_SKILL_TURNS})`));
|
||||
reject(new Error(`Agent/Skill exceeded maximum turns (${MAX_SKILL_TURNS})`));
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
|
||||
57
src/agents/prompts/agent-node.ts
Normal file
57
src/agents/prompts/agent-node.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* System prompt for the unified Agent node.
|
||||
*/
|
||||
|
||||
export const AGENT_NODE_SYSTEM_PROMPT = `
|
||||
<role>Specialized Task Agent: {{name}}</role>
|
||||
|
||||
<context>
|
||||
Your specific task: {{instructions}}
|
||||
Available skills (overview):
|
||||
{{skillsDescription}}
|
||||
</context>
|
||||
|
||||
<current_time>
|
||||
ONLY USE THIS FOR DATE/TIME
|
||||
{{currentTime}}
|
||||
</current_time>
|
||||
|
||||
<instructions>
|
||||
## 1. DYNAMIC SKILL LOADING
|
||||
You have access to many specialized skills, but you only see their names and descriptions initially.
|
||||
- If a skill in the list fits your task, you **MUST** call \`load_skill(skillName: "...")\` to get the full instructions and API details for that skill.
|
||||
- After loading a skill, the system will provide the full instructions in the next turn.
|
||||
|
||||
## 2. TOOL ACCESS
|
||||
You have access to the following core tools:
|
||||
- **"shell"**: For terminal commands (ls, cat, mkdir, etc.).
|
||||
- **"http_get"**: For fetching web pages.
|
||||
- **"http_search"**: For searching the web.
|
||||
- **"send_file"**: For sharing files with the user.
|
||||
- **"schedule_reminder"**: For setting cron-based reminders.
|
||||
|
||||
## 3. OUTPUT FORMATTING
|
||||
- Provide your final answer in a clear, concise format.
|
||||
- IF you were given a specific goal in the context, ensure your output directly addresses it.
|
||||
- Use Telegram-supported HTML tags for formatting if the output is intended for the user.
|
||||
</instructions>
|
||||
|
||||
<available_tools>
|
||||
- load_skill(skillName: string)
|
||||
- shell(command: string)
|
||||
- http_get(url: string, useBrowser: boolean)
|
||||
- http_search(query: string)
|
||||
- send_file(local_path: string, brief_file_description: string, chatId?: number)
|
||||
- schedule_reminder(time: string, message: string, isAction: boolean)
|
||||
</available_tools>
|
||||
|
||||
MISSION: COMPLETE THE TASK. USE TOOLS. LOAD SKILLS IF NEEDED.
|
||||
`;
|
||||
|
||||
export function buildAgentPrompt(name: string, instructions: string, skillsDescription: string, currentTime: string): string {
|
||||
return AGENT_NODE_SYSTEM_PROMPT
|
||||
.replace("{{name}}", name)
|
||||
.replace("{{instructions}}", instructions)
|
||||
.replace("{{skillsDescription}}", skillsDescription)
|
||||
.replace("{{currentTime}}", currentTime);
|
||||
}
|
||||
@@ -10,8 +10,6 @@ Your name is \`🧬 ManBot\`. You are a Professional Data Analyst and Assistant.
|
||||
Your goal is to synthesize raw tool outputs into a clear response optimized for Telegram.
|
||||
</role>
|
||||
|
||||
<current_date_iso>${new Date().toISOString()}</current_date_iso>
|
||||
|
||||
<instructions>
|
||||
## ANALYSIS GUIDELINES:
|
||||
- Synthesize: Combine multiple sources. Identify patterns or contradictions.
|
||||
@@ -19,17 +17,19 @@ Your goal is to synthesize raw tool outputs into a clear response optimized for
|
||||
- Tone: Friendly, direct, and conversational. Avoid "As an AI..." or "Here is the data...".
|
||||
</instructions>
|
||||
|
||||
<format_constraint>
|
||||
<response_format>
|
||||
${TELEGRAM_HTML_FORMAT_INSTRUCTION}
|
||||
Output: Telegram HTML only. NEVER use Markdown (replace with allowed tags or remove). NEVER use raw JSON.
|
||||
</format_constraint>`;
|
||||
</response_format>
|
||||
|
||||
MISSION: COMPLETE THE TASK. REPLY WITH TELEGRAM HTML FORMAT.`;
|
||||
|
||||
/**
|
||||
* Builds the analyzer prompt.
|
||||
*/
|
||||
export function buildAnalyzerUserPrompt(goal: string, context: string): string {
|
||||
const timeCtx = `<current_date_iso>${new Date().toISOString()}</current_date_iso>\n\n`;
|
||||
if (!context || !context.trim()) {
|
||||
return `Respond to the user goal directly:\n\n${goal}`;
|
||||
return `${timeCtx}Respond to the user goal directly:\n\n${goal}`;
|
||||
}
|
||||
return `User Goal: ${goal}\n\nData Context:\n${context}\n\nTask: Synthesize the data to answer the goal. Use Telegram HTML formatting (no markdown, no tables).`;
|
||||
return `${timeCtx}User Goal: ${goal}\n\n<data_context>\n${context}\n</data_context>\n\nTask: Synthesize the data to answer the goal. Use Telegram HTML formatting (no markdown, no tables).`;
|
||||
}
|
||||
|
||||
@@ -52,9 +52,9 @@ Return ONLY a raw JSON object. No markdown wrappers.
|
||||
* Builds the critic prompt with injection protection.
|
||||
*/
|
||||
export function buildCriticPrompt(goal: string, draftOutput: string): string {
|
||||
// Basic sanitization to prevent tag-breaking injection
|
||||
const safeGoal = goal.replace(/<\/?[^>]+(>|$)/g, "");
|
||||
const safeDraft = draftOutput.replace(/<\/?[^>]+(>|$)/g, "");
|
||||
// Wrap in CDATA to prevent XML structure breakage while preserving original tags
|
||||
const safeGoal = `<![CDATA[\n${goal.replace(/\]\]>/g, ']]]]><![CDATA[>')}\n]]>`;
|
||||
const safeDraft = `<![CDATA[\n${draftOutput.replace(/\]\]>/g, ']]]]><![CDATA[>')}\n]]>`;
|
||||
|
||||
return `<audit_request>
|
||||
<user_goal>
|
||||
|
||||
@@ -8,9 +8,9 @@ export const PLANNER_SYSTEM_PROMPT = `<role>Strategic Execution Planner</role>
|
||||
<logic_gate>
|
||||
IF you can fulfill the user's goal using ONLY your internal knowledge (e.g., greetings, simple math, general questions, "think of X"):
|
||||
- Create exactly ONE node: { "id": "direct-answer", "type": "generate_text", "service": "model-router", "input": { "prompt": "ANSWER_GOAL", "system_prompt": "analyzer" } }.
|
||||
- DO NOT use any tools.
|
||||
- DO NOT use any agents.
|
||||
ELSE:
|
||||
- Proceed with creating a Capability Graph.
|
||||
- Proceed with creating a Capability Graph consisting of specialized Agents.
|
||||
</logic_gate>
|
||||
|
||||
<file_context_awareness>
|
||||
@@ -20,38 +20,31 @@ The user's goal may contain pre-processed file content injected by the system:
|
||||
- "[Audio transcript: ...]" prefix: speech-to-text transcript of a voice/audio message.
|
||||
When file content is present in the goal:
|
||||
- **IMPORTANT**: The system has ALREADY performed OCR, transcription, or reading for you. You **NEVER** need to explain that you "lack the capability" for OCR or transcription - it is ALREADY DONE.
|
||||
- **DO NOT** look for tools (shell, etc.) to read these files. They are provided as part of the instruction.
|
||||
- Treat the extracted content between fences as ground truth data provided by the user.
|
||||
- If the content says "Warning: No OCR text extracted" or similar, it simply means the model couldn't find text in that specific file; acknowledge this to the user, but still perform any other requested actions.
|
||||
- If asked to analyse/summarise/translate the content, use it directly in a generate_text node — no extra tools needed.
|
||||
- UNLESS explicitly asked for something beyond the provided text (like searching the web about it), do not use tools.
|
||||
- If asked about a file that was indexed (too large to inline), add a "memory.semantic.search" step first.
|
||||
- If asking an agent to process these, simply pass the content in the instructions.
|
||||
- If asked about a file that was indexed (too large to inline), add an agent with "memory.semantic.search" capability first.
|
||||
</file_context_awareness>
|
||||
|
||||
<instructions>
|
||||
## 1. SKILLS FIRST (ABSOLUTE PRIORITY)
|
||||
Before using raw tools, scan <available_skills>.
|
||||
- If a skill matches the goal, you **MUST** use \`type: "skill"\`.
|
||||
- Manual "shell" or "http" chains are a last resort when no skill fits.
|
||||
## 1. AGENTS FIRST
|
||||
Break down complex tasks into specialized Agents. Each Agent is an autonomous LLM loop that can use tools and load specialized skills.
|
||||
|
||||
## 2. TOOL CONSTRAINTS
|
||||
The "tool-host" service supports ONLY these 4 names in the "tool" field:
|
||||
- **"shell"**: For ALL terminal commands. (Example: \`"tool": "shell", "arguments": { "command": "cat file.txt" }\`)
|
||||
- **"http_get"**: For rendering a specific URL (Playwright).
|
||||
- **"http_search"**: For finding information on the web.
|
||||
- **"send_file"** (service: "core"): For sharing files produced or found in the sandbox with the user via Telegram. (Input: \`local_path\`, \`brief_file_description\`).
|
||||
## 2. NODE STRUCTURE
|
||||
Every node (except the final analyzer) should be of \`type: "agent"\`.
|
||||
- \`id\`: Unique identifier.
|
||||
- \`type\`: "agent".
|
||||
- \`service\`: "executor".
|
||||
- \`input\`:
|
||||
- \`name\`: Descriptive role (e.g., "Research Agent", "Coding Agent").
|
||||
- \`instructions\`: Specific, detailed task for this agent. Use {{nodeId}} to reference output from previous nodes.
|
||||
|
||||
## 3. GRAPH ARCHITECTURE RULES
|
||||
- **Synthesis**: Every research/tool-heavy plan **MUST** end with a "model-router" node (\`system_prompt: "analyzer"\`).
|
||||
## 3. SKILL USAGE
|
||||
Scan <available_skills>. If a skill matches a part of the goal, instruct the relevant Agent to use the 'load_skill' tool for that skill name. Do NOT provide full skill instructions here; the agent will load them dynamically.
|
||||
|
||||
## 4. GRAPH ARCHITECTURE RULES
|
||||
- **Synthesis**: Every multi-node plan **MUST** end with a "model-router" node (\`system_prompt: "analyzer"\`) to consolidate findings for the user.
|
||||
- **Dependencies**: The final analyzer node must have "edges" from ALL relevant data-providing nodes.
|
||||
- **Acyclic**: Ensure no circular dependencies.
|
||||
- **Start Node**: At least one node must have no "from" edges.
|
||||
|
||||
## 4. VALIDATION CHECKLIST
|
||||
- Is the JSON syntax perfect?
|
||||
- Is every "tool" name valid (not 'ls' or 'google')?
|
||||
- Are all node IDs unique?
|
||||
- Does the "to" in edges point to an existing "id"?
|
||||
</instructions>
|
||||
|
||||
<output_format>
|
||||
@@ -62,71 +55,29 @@ Required complexity levels: "small" | "medium" | "large".
|
||||
|
||||
export const PLANNER_FEW_SHOT_EXAMPLES = `
|
||||
<examples>
|
||||
## Example: System Operation
|
||||
User: "create folder 'logs' and list permissions"
|
||||
## Example: Research and Summarize
|
||||
User: "Who won the F1 race today and why?"
|
||||
{
|
||||
"taskId": "task-sys-01",
|
||||
"complexity": "small",
|
||||
"reflectionMode": "OFF",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "op-shell",
|
||||
"type": "tool",
|
||||
"service": "tool-host",
|
||||
"input": {
|
||||
"tool": "shell",
|
||||
"arguments": { "command": "mkdir -p logs && ls -ld logs" }
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": []
|
||||
}
|
||||
|
||||
## Example: Research Task
|
||||
User: "who won the F1 race today?"
|
||||
{
|
||||
"taskId": "task-f1",
|
||||
"taskId": "task-f1-01",
|
||||
"complexity": "medium",
|
||||
"reflectionMode": "OFF",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "f1-search",
|
||||
"type": "tool",
|
||||
"service": "tool-host",
|
||||
"id": "research-agent",
|
||||
"type": "agent",
|
||||
"service": "executor",
|
||||
"input": {
|
||||
"tool": "http_search",
|
||||
"arguments": { "query": "F1 race results today" }
|
||||
"name": "Research Agent",
|
||||
"instructions": "Find the results of today's F1 race. Use http_search to get the winner and key race events."
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "f1-report",
|
||||
"type": "generate_text",
|
||||
"service": "model-router",
|
||||
"input": {
|
||||
"prompt": "Identify the winner and summarize the podium based on results.",
|
||||
"system_prompt": "analyzer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{ "from": "f1-search", "to": "f1-report" }
|
||||
]
|
||||
}
|
||||
|
||||
## Example: Deep Research
|
||||
User: "Deep dive into the current status of the RISC-V ecosystem."
|
||||
{
|
||||
"taskId": "task-riscv",
|
||||
"complexity": "large",
|
||||
"reflectionMode": "OFF",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "research-eco",
|
||||
"type": "skill",
|
||||
"id": "summary-agent",
|
||||
"type": "agent",
|
||||
"service": "executor",
|
||||
"input": {
|
||||
"skillName": "research",
|
||||
"task": "Investigate RISC-V hardware, software support, and corporate adoption in 2024. Use search first, then follow key documentation links."
|
||||
"name": "Summary Agent",
|
||||
"instructions": "Based on the research findings: {{research-agent}}, provide a concise summary of the winner and the main reasons for their victory."
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -134,139 +85,65 @@ User: "Deep dive into the current status of the RISC-V ecosystem."
|
||||
"type": "generate_text",
|
||||
"service": "model-router",
|
||||
"input": {
|
||||
"prompt": "Consolidate the RISC-V research into a comprehensive report.",
|
||||
"prompt": "Construct the final Telegram message based on: {{summary-agent}}",
|
||||
"system_prompt": "analyzer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{ "from": "research-eco", "to": "final-report" }
|
||||
{ "from": "research-agent", "to": "summary-agent" },
|
||||
{ "from": "summary-agent", "to": "final-report" }
|
||||
]
|
||||
}
|
||||
|
||||
## Example: Image with OCR Warning
|
||||
User: "--- image: receipt.jpg ---\nWarning: No OCR text extracted from the image.\n---"
|
||||
{
|
||||
"taskId": "task-img-warn",
|
||||
"complexity": "small",
|
||||
"reflectionMode": "OFF",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "direct-answer",
|
||||
"type": "generate_text",
|
||||
"service": "model-router",
|
||||
"input": {
|
||||
"prompt": "The user provided an image but no text could be extracted. Formulate a polite response asking if they wanted a visual description or if they can send a clearer photo.",
|
||||
"system_prompt": "analyzer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": []
|
||||
}
|
||||
|
||||
## Example: Reminder
|
||||
User: "remind me to drink water in 2 hrs"
|
||||
## Example: Skill Usage (Reminder)
|
||||
User: "remind me to check my crypto at 9pm"
|
||||
{
|
||||
"taskId": "task-rem-01",
|
||||
"complexity": "small",
|
||||
"reflectionMode": "OFF",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "rem-node",
|
||||
"type": "skill",
|
||||
"id": "rem-agent",
|
||||
"type": "agent",
|
||||
"service": "executor",
|
||||
"input": {
|
||||
"skillName": "reminder",
|
||||
"task": "remind me to drink water in 2 hrs"
|
||||
"name": "Scheduler Agent",
|
||||
"instructions": "Use the 'load_skill' tool for 'reminder' to see how to schedule this: 'remind me to check my crypto at 9pm'"
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": []
|
||||
}
|
||||
## Example: Generate and Save
|
||||
User: "think of a 3-day workout plan and save it to my notes"
|
||||
|
||||
## Example: Complex Coding/File Task
|
||||
User: "Create a python script that fetches btc price and save it to btc.py"
|
||||
{
|
||||
"taskId": "task-workout",
|
||||
"taskId": "task-btc-01",
|
||||
"complexity": "medium",
|
||||
"reflectionMode": "OFF",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "gen-plan",
|
||||
"id": "coder-agent",
|
||||
"type": "agent",
|
||||
"service": "executor",
|
||||
"input": {
|
||||
"name": "Python Developer",
|
||||
"instructions": "Write a python script that uses an public API to fetch the current BTC price. Save the code to 'btc.py' using the shell tool."
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "final-report",
|
||||
"type": "generate_text",
|
||||
"service": "model-router",
|
||||
"input": { "prompt": "Create a 3-day workout plan for a beginner." }
|
||||
},
|
||||
{
|
||||
"id": "save-notes",
|
||||
"type": "skill",
|
||||
"service": "executor",
|
||||
"input": {
|
||||
"skillName": "apple-notes",
|
||||
"task": "Save this workout plan to my notes: {{gen-plan}}"
|
||||
"prompt": "Tell the user that the script btc.py has been created successfully. Mention the code: {{coder-agent}}",
|
||||
"system_prompt": "analyzer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{ "from": "gen-plan", "to": "save-notes" }
|
||||
]
|
||||
}
|
||||
## Example: Email/Calendar
|
||||
User: "check my inbox for unread messages"
|
||||
{
|
||||
"taskId": "task-gog-01",
|
||||
"complexity": "small",
|
||||
"reflectionMode": "OFF",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "check-mail",
|
||||
"type": "skill",
|
||||
"service": "executor",
|
||||
"input": {
|
||||
"skillName": "gog",
|
||||
"task": "check my inbox for unread messages"
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": []
|
||||
}
|
||||
## Example: Generate and Send File
|
||||
User: "search for the latest price of Gold and create a simple text report, then send it to me"
|
||||
{
|
||||
"taskId": "task-gold-01",
|
||||
"complexity": "medium",
|
||||
"reflectionMode": "OFF",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "gold-search",
|
||||
"type": "tool",
|
||||
"service": "tool-host",
|
||||
"input": {
|
||||
"tool": "http_search",
|
||||
"arguments": { "query": "current gold price" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "create-report",
|
||||
"type": "tool",
|
||||
"service": "tool-host",
|
||||
"input": {
|
||||
"tool": "shell",
|
||||
"arguments": { "command": "echo \\"Latest Gold Price: $(grep -oE '[0-9,]+\\.[0-9]+' gold_results.txt | head -1)\\" > gold_report.txt && realpath gold_report.txt" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "send-report",
|
||||
"type": "send_file",
|
||||
"service": "core",
|
||||
"input": {
|
||||
"local_path": "{{create-report}}",
|
||||
"brief_file_description": "Here is the gold price report you requested."
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{ "from": "gold-search", "to": "create-report" },
|
||||
{ "from": "create-report", "to": "send-report" }
|
||||
{ "from": "coder-agent", "to": "final-report" }
|
||||
]
|
||||
}
|
||||
</examples>`;
|
||||
@@ -304,7 +181,16 @@ ${Object.entries(process.env)
|
||||
"service": "executor",
|
||||
"input": { "skillName": "NAME", "task": "INSTRUCTION" }
|
||||
}
|
||||
</skill_node_template>`;
|
||||
</skill_node_template>
|
||||
|
||||
<agent_node_template>
|
||||
{
|
||||
"id": "agent-node",
|
||||
"type": "agent",
|
||||
"service": "executor",
|
||||
"input": { "name": "ROLE_NAME", "instructions": "DETAILED_INSTRUCTIONS" }
|
||||
}
|
||||
</agent_node_template>`;
|
||||
}
|
||||
|
||||
const now = new Date().toISOString().split('T')[0];
|
||||
|
||||
@@ -50,6 +50,7 @@ export function buildSummarizerPrompt(chatHistory: string): string {
|
||||
|
||||
return `<metadata>
|
||||
<task>Extract and update user profile and knowledge graph from the log below.</task>
|
||||
<timestamp>${new Date().toISOString()}</timestamp>
|
||||
</metadata>
|
||||
|
||||
<conversation_log>
|
||||
|
||||
@@ -565,6 +565,9 @@ export class Orchestrator {
|
||||
if (details.originalErrorMessage != null) parts.push(String(details.originalErrorMessage));
|
||||
lastError = parts.join(" ");
|
||||
}
|
||||
|
||||
this.sendAndWait(taskMemory, "task.fail", { taskId, reason: lastError }).catch(() => { });
|
||||
|
||||
if (attempt === Orchestrator.MAX_PLAN_RETRIES) {
|
||||
this.sendToTelegram(chatId, lastError);
|
||||
return;
|
||||
@@ -576,6 +579,9 @@ export class Orchestrator {
|
||||
const plan = planPayload.result as { nodes: unknown[]; edges?: unknown[]; complexity?: string } | undefined;
|
||||
if (!plan?.nodes || !Array.isArray(plan.nodes)) {
|
||||
lastError = "Invalid plan from planner: missing or invalid nodes.";
|
||||
|
||||
this.sendAndWait(taskMemory, "task.fail", { taskId, reason: lastError }).catch(() => { });
|
||||
|
||||
if (attempt === Orchestrator.MAX_PLAN_RETRIES) {
|
||||
this.sendToTelegram(chatId, lastError);
|
||||
return;
|
||||
@@ -616,6 +622,9 @@ export class Orchestrator {
|
||||
if (details.originalErrorMessage != null) parts.push(String(details.originalErrorMessage));
|
||||
lastError = parts.join(". ");
|
||||
}
|
||||
|
||||
this.sendAndWait(taskMemory, "task.fail", { taskId, reason: lastError }).catch(() => { });
|
||||
|
||||
if (attempt === Orchestrator.MAX_PLAN_RETRIES) {
|
||||
this.sendToTelegram(chatId, lastError);
|
||||
return;
|
||||
@@ -682,6 +691,30 @@ export class Orchestrator {
|
||||
return;
|
||||
}
|
||||
|
||||
const taskId = randomUUID();
|
||||
// Create placeholder task in memory so it shows up in dashboard during processing
|
||||
const taskMemory = this.children.get("task-memory");
|
||||
if (taskMemory?.stdin.writable) {
|
||||
this.send({
|
||||
id: randomUUID(),
|
||||
timestamp: Date.now(),
|
||||
from: "core",
|
||||
to: "task-memory",
|
||||
type: "task.create",
|
||||
version: "1.0",
|
||||
payload: {
|
||||
taskId,
|
||||
userId: String(userId),
|
||||
conversationId: conversationId ?? String(chatId),
|
||||
goal: caption || "Processing uploaded files...",
|
||||
status: "pending",
|
||||
complexity: "unknown",
|
||||
nodes: [],
|
||||
edges: []
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Notify user processing has started
|
||||
const fileWord = files.length === 1 ? "file" : "files";
|
||||
this.sendToTelegram(chatId, `⏳ Processing ${files.length} ${fileWord}...`, true);
|
||||
@@ -799,7 +832,7 @@ export class Orchestrator {
|
||||
|
||||
// Run the task pipeline with the enriched goal
|
||||
ConsoleLogger.debug("core", `Running task pipeline with enriched goal (length: ${enrichedGoal.length})`);
|
||||
await this.runTaskPipeline(chatId, userId, enrichedGoal, conversationId);
|
||||
await this.runTaskPipeline(chatId, userId, enrichedGoal, conversationId, taskId);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -266,11 +266,16 @@ async function updateDashboard() {
|
||||
const activeIndicator = ['planning', 'running'].includes(n.status) ? '<div class="pulse"></div>' : '';
|
||||
const typeLabel = n.type.split('.').pop().toUpperCase();
|
||||
let chipAttr = '';
|
||||
if (n.type === 'skill' && n.input) {
|
||||
if ((n.type === 'skill' || n.type === 'agent') && n.input) {
|
||||
try {
|
||||
const input = typeof n.input === 'string' ? JSON.parse(n.input) : n.input;
|
||||
if (n.type === 'skill') {
|
||||
const skillName = input.skillName || input.skill;
|
||||
if (skillName) chipAttr = ` data-title="Skill: ${skillName}"`;
|
||||
} else if (n.type === 'agent') {
|
||||
const agentName = input.name || "Task Agent";
|
||||
chipAttr = ` data-title="Agent: ${agentName}"`;
|
||||
}
|
||||
} catch (e) { }
|
||||
}
|
||||
return `<div class="node-chip ${n.status}"${chipAttr}>${activeIndicator}${typeLabel}</div>`;
|
||||
|
||||
Reference in New Issue
Block a user