mirror of
https://github.com/larchanka/manbot.git
synced 2026-05-13 21:42:08 +00:00
Compare commits
36 Commits
v1.0.20260
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b10003c43 | ||
|
|
3d0d5b0d27 | ||
|
|
ae454123a2 | ||
|
|
42f537eb4a | ||
|
|
bbf7526940 | ||
|
|
6a69da9495 | ||
|
|
bdd8fee826 | ||
|
|
0255123229 | ||
|
|
6517c45992 | ||
|
|
cc08878c09 | ||
|
|
c354f55a14 | ||
|
|
3d17545dcd | ||
|
|
645ef8701e | ||
|
|
242604675a | ||
|
|
0360275eea | ||
|
|
8e395df9ac | ||
|
|
d2bc7f3da2 | ||
|
|
3c85eef2bf | ||
|
|
dea80b30a1 | ||
|
|
0f0eb1a024 | ||
|
|
28f3b6bba4 | ||
|
|
6d3c6f3e21 | ||
|
|
54bc04922f | ||
|
|
1e13b2ec32 | ||
|
|
99794fec95 | ||
|
|
9820566567 | ||
|
|
a1a59a610a | ||
|
|
56f0c965d0 | ||
|
|
6501f232ae | ||
|
|
9d70ecc805 | ||
|
|
35e32c8e4b | ||
|
|
9e021f2593 | ||
|
|
4859ef5de7 | ||
|
|
432f2eca05 | ||
|
|
5cceb959a8 | ||
|
|
9b949f7a3a |
@@ -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
|
||||
|
||||
---
|
||||
|
||||
|
||||
711
_docs/IMPROVEMENTS.md
Normal file
711
_docs/IMPROVEMENTS.md
Normal file
@@ -0,0 +1,711 @@
|
||||
# ManBot AI Platform - Proposed Improvements
|
||||
|
||||
## Overview
|
||||
This document outlines comprehensive improvements for the ManBot AI platform based on architectural analysis, code quality assessment, and performance considerations.
|
||||
|
||||
## Project Analysis Summary
|
||||
|
||||
ManBot is a sophisticated multi-process AI platform with type-safe IPC and capability-graph execution. It demonstrates excellent architectural foundation with room for refinement in type safety, performance optimization, and developer experience.
|
||||
|
||||
### Current Strengths
|
||||
- **Multi-agent pipeline**: Well-structured Planner → Task Memory → Executor → Critic flow
|
||||
- **Type-safe IPC**: Zod-validated envelopes with JSONL communication
|
||||
- **Capability graph**: DAG-based execution with parallel processing
|
||||
- **Modular design**: Clear separation between adapters, agents, services, and shared utilities
|
||||
- **Local-first approach**: No cloud dependencies for core AI functionality
|
||||
- **Comprehensive feature set**: File processing, reminders, RAG, monitoring dashboard
|
||||
|
||||
### Key Metrics from Analysis
|
||||
- **Test coverage**: 17 test files covering core functionality
|
||||
- **Type safety concerns**: 396+ instances of `any/unknown` types
|
||||
- **Import complexity**: 126+ relative imports using `../` patterns
|
||||
- **Async usage**: 727+ async/await patterns for optimization opportunities
|
||||
- **Error handling**: 66+ try/catch/throw patterns across services
|
||||
|
||||
---
|
||||
|
||||
## Proposed Improvements
|
||||
|
||||
### 1. Code Organization & Maintainability
|
||||
|
||||
#### 1.1 Type Safety Improvements
|
||||
**Priority**: High
|
||||
**Impact**: Code reliability and developer experience
|
||||
|
||||
**Current Issues:**
|
||||
- 396+ matches for `any/unknown` types across 41 files
|
||||
- Missing type definitions in protocol schemas
|
||||
- Inconsistent error type handling
|
||||
|
||||
**Proposed Actions:**
|
||||
```typescript
|
||||
// Replace any types with proper interfaces
|
||||
// Before
|
||||
const data: any = response.data;
|
||||
|
||||
// After
|
||||
interface ApiResponse<T> {
|
||||
data: T;
|
||||
status: number;
|
||||
message?: string;
|
||||
}
|
||||
const data: ApiResponse<ExpectedType> = response.data;
|
||||
```
|
||||
|
||||
**Implementation Steps:**
|
||||
1. Audit all `any` types in `src/shared/protocol.ts`
|
||||
2. Create comprehensive interfaces for all API responses
|
||||
3. Add generic type parameters for reusable components
|
||||
4. Implement strict error type hierarchy
|
||||
|
||||
#### 1.2 Import Path Optimization
|
||||
**Priority**: High
|
||||
**Impact**: Code maintainability and refactoring
|
||||
|
||||
**Current Issues:**
|
||||
- 126+ relative imports using `../` patterns
|
||||
- Deep import paths making refactoring difficult
|
||||
- Inconsistent import organization
|
||||
|
||||
**Proposed Actions:**
|
||||
```typescript
|
||||
// tsconfig.json additions
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@shared/*": ["src/shared/*"],
|
||||
"@agents/*": ["src/agents/*"],
|
||||
"@services/*": ["src/services/*"],
|
||||
"@adapters/*": ["src/adapters/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Replace relative imports
|
||||
// Before
|
||||
import { Envelope } from "../shared/protocol.js";
|
||||
import { BaseProcess } from "../shared/base-process.js";
|
||||
|
||||
// After
|
||||
import { Envelope } from "@shared/protocol.js";
|
||||
import { BaseProcess } from "@shared/base-process.js";
|
||||
```
|
||||
|
||||
**Implementation Steps:**
|
||||
1. Configure path mapping in `tsconfig.json`
|
||||
2. Create barrel exports in major directories
|
||||
3. Systematically replace relative imports
|
||||
4. Update build configuration
|
||||
|
||||
#### 1.3 Error Handling Standardization
|
||||
**Priority**: Medium
|
||||
**Impact**: Debugging and user experience
|
||||
|
||||
**Current Issues:**
|
||||
- Inconsistent error types across services
|
||||
- Missing error context in some cases
|
||||
- No centralized error handling patterns
|
||||
|
||||
**Proposed Actions:**
|
||||
```typescript
|
||||
// Create centralized error types
|
||||
export class ManBotError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly code: string,
|
||||
public readonly service: string,
|
||||
public readonly context?: Record<string, unknown>
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'ManBotError';
|
||||
}
|
||||
}
|
||||
|
||||
// Service-specific error classes
|
||||
export class LemonadeError extends ManBotError {
|
||||
constructor(message: string, context?: Record<string, unknown>) {
|
||||
super(message, 'LEMONADE_ERROR', 'lemonade-adapter', context);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Performance Optimizations
|
||||
|
||||
#### 2.1 Browser Service Enhancement
|
||||
**Priority**: Medium
|
||||
**Impact**: Web scraping performance and resource usage
|
||||
|
||||
**Current Issues:**
|
||||
- Each request spawns new browser instance (100-200MB per instance)
|
||||
- No connection pooling for multiple requests
|
||||
- Potential memory leaks with browser contexts
|
||||
|
||||
**Proposed Actions:**
|
||||
```typescript
|
||||
// Browser connection pool
|
||||
class BrowserPool {
|
||||
private browsers: Browser[] = [];
|
||||
private maxBrowsers = 3;
|
||||
private requestQueue: Array<{
|
||||
url: string;
|
||||
resolve: (result: any) => void;
|
||||
reject: (error: Error) => void;
|
||||
}> = [];
|
||||
|
||||
async executeRequest(url: string): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.requestQueue.push({ url, resolve, reject });
|
||||
this.processQueue();
|
||||
});
|
||||
}
|
||||
|
||||
private async processQueue() {
|
||||
if (this.requestQueue.length === 0) return;
|
||||
|
||||
const browser = await this.getAvailableBrowser();
|
||||
const request = this.requestQueue.shift();
|
||||
|
||||
if (request) {
|
||||
this.executeWithBrowser(browser, request);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.2 Model Management Optimization
|
||||
**Priority**: Medium
|
||||
**Impact**: AI model response times and resource utilization
|
||||
|
||||
**Current Issues:**
|
||||
- Cold start delays for large models
|
||||
- No intelligent preloading based on usage patterns
|
||||
- Fixed keep-alive durations regardless of usage
|
||||
|
||||
**Proposed Actions:**
|
||||
```typescript
|
||||
// Smart model manager
|
||||
class SmartModelManager {
|
||||
private usagePatterns = new Map<string, {
|
||||
lastUsed: Date;
|
||||
frequency: number;
|
||||
avgResponseTime: number;
|
||||
}>();
|
||||
|
||||
predictModelUsage(modelName: string): number {
|
||||
const pattern = this.usagePatterns.get(modelName);
|
||||
if (!pattern) return 0;
|
||||
|
||||
const hoursSinceLastUse = (Date.now() - pattern.lastUsed.getTime()) / (1000 * 60 * 60);
|
||||
const frequencyScore = pattern.frequency / 24; // requests per hour
|
||||
const recencyScore = Math.max(0, 1 - hoursSinceLastUse / 24);
|
||||
|
||||
return frequencyScore * recencyScore;
|
||||
}
|
||||
|
||||
optimizeKeepAlive(modelName: string): string {
|
||||
const prediction = this.predictModelUsage(modelName);
|
||||
if (prediction > 0.5) return "2h";
|
||||
if (prediction > 0.2) return "30m";
|
||||
return "10m";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.3 Async Pattern Optimization
|
||||
**Priority**: Medium
|
||||
**Impact**: System responsiveness and throughput
|
||||
|
||||
**Current Issues:**
|
||||
- 727+ async/await patterns that could be optimized
|
||||
- Some sequential operations that could be parallelized
|
||||
- Missing Promise.all optimizations
|
||||
|
||||
**Proposed Actions:**
|
||||
```typescript
|
||||
// Before: Sequential operations
|
||||
async function processFiles(files: string[]) {
|
||||
const results = [];
|
||||
for (const file of files) {
|
||||
const result = await processFile(file);
|
||||
results.push(result);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
// After: Parallel operations with batching
|
||||
async function processFiles(files: string[], batchSize = 5) {
|
||||
const results = [];
|
||||
for (let i = 0; i < files.length; i += batchSize) {
|
||||
const batch = files.slice(i, i + batchSize);
|
||||
const batchResults = await Promise.all(
|
||||
batch.map(file => processFile(file))
|
||||
);
|
||||
results.push(...batchResults);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Security & Dependencies
|
||||
|
||||
#### 3.1 Dependency Security Audit
|
||||
**Priority**: High
|
||||
**Impact**: Security posture and vulnerability management
|
||||
|
||||
**Proposed Actions:**
|
||||
```bash
|
||||
# Security audit commands
|
||||
npm audit --audit-level moderate
|
||||
npm audit fix
|
||||
npm update
|
||||
|
||||
# Add to package.json scripts
|
||||
{
|
||||
"scripts": {
|
||||
"security:audit": "npm audit --audit-level moderate",
|
||||
"security:check": "npm outdated",
|
||||
"security:fix": "npm audit fix && npm update"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation Steps:**
|
||||
1. Run comprehensive dependency audit
|
||||
2. Update vulnerable packages
|
||||
3. Implement automated security scanning in CI/CD
|
||||
4. Create dependency update policy
|
||||
|
||||
#### 3.2 Environment Configuration Validation
|
||||
**Priority**: Medium
|
||||
**Impact**: Runtime reliability and deployment safety
|
||||
|
||||
**Proposed Actions:**
|
||||
```typescript
|
||||
// Runtime configuration validation
|
||||
import { z } from "zod";
|
||||
|
||||
const configSchema = z.object({
|
||||
telegram: z.object({
|
||||
botToken: z.string().min(1, "Telegram bot token is required"),
|
||||
allowedUserIds: z.string().optional(),
|
||||
}),
|
||||
lemonade: z.object({
|
||||
baseUrl: z.string().url("Invalid Lemonade base URL"),
|
||||
timeout: z.number().min(1000, "Timeout must be at least 1 second"),
|
||||
}),
|
||||
rag: z.object({
|
||||
embedModel: z.string().min(1),
|
||||
dbPath: z.string().min(1),
|
||||
embeddingDimensions: z.number().positive(),
|
||||
}),
|
||||
});
|
||||
|
||||
export function validateConfig(config: unknown) {
|
||||
return configSchema.parse(config);
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.3 Input Sanitization Enhancement
|
||||
**Priority**: Medium
|
||||
**Impact**: Security and data integrity
|
||||
|
||||
**Proposed Actions:**
|
||||
```typescript
|
||||
// Input sanitization utilities
|
||||
class InputSanitizer {
|
||||
static sanitizeString(input: unknown, maxLength = 10000): string {
|
||||
if (typeof input !== 'string') {
|
||||
throw new Error('Input must be a string');
|
||||
}
|
||||
|
||||
return input
|
||||
.trim()
|
||||
.slice(0, maxLength)
|
||||
.replace(/[<>]/g, ''); // Basic HTML tag removal
|
||||
}
|
||||
|
||||
static sanitizeFilePath(path: unknown): string {
|
||||
const cleanPath = this.sanitizeString(path, 255);
|
||||
// Prevent path traversal
|
||||
return cleanPath.replace(/\.\./g, '').replace(/^\//, '');
|
||||
}
|
||||
|
||||
static validateTelegramUserId(userId: unknown): number {
|
||||
const id = Number(userId);
|
||||
if (!Number.isInteger(id) || id <= 0) {
|
||||
throw new Error('Invalid Telegram user ID');
|
||||
}
|
||||
return id;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Testing & Quality Assurance
|
||||
|
||||
#### 4.1 End-to-End Testing Framework
|
||||
**Priority**: Medium
|
||||
**Impact**: System reliability and regression prevention
|
||||
|
||||
**Proposed Actions:**
|
||||
```typescript
|
||||
// E2E test framework
|
||||
class E2ETestSuite {
|
||||
async testCompleteWorkflow(userInput: string) {
|
||||
// 1. Send message to Telegram adapter
|
||||
const response = await this.sendTelegramMessage(userInput);
|
||||
|
||||
// 2. Verify task creation
|
||||
const task = await this.waitForTaskCreation();
|
||||
expect(task.goal).toContain(userInput);
|
||||
|
||||
// 3. Monitor execution pipeline
|
||||
const execution = await this.monitorExecution(task.id);
|
||||
expect(execution.status).toBe('completed');
|
||||
|
||||
// 4. Verify response delivery
|
||||
const finalResponse = await this.waitForTelegramResponse();
|
||||
expect(finalResponse).toBeDefined();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.2 Performance Benchmarking
|
||||
**Priority**: Medium
|
||||
**Impact**: Performance regression detection
|
||||
|
||||
**Proposed Actions:**
|
||||
```typescript
|
||||
// Performance benchmarks
|
||||
describe('Performance Benchmarks', () => {
|
||||
it('should process simple task within 5 seconds', async () => {
|
||||
const start = Date.now();
|
||||
await processSimpleTask('Hello world');
|
||||
const duration = Date.now() - start;
|
||||
expect(duration).toBeLessThan(5000);
|
||||
});
|
||||
|
||||
it('should handle concurrent requests efficiently', async () => {
|
||||
const requests = Array(10).fill(null).map(() =>
|
||||
processSimpleTask('Concurrent test')
|
||||
);
|
||||
|
||||
const start = Date.now();
|
||||
await Promise.all(requests);
|
||||
const duration = Date.now() - start;
|
||||
|
||||
// Should be significantly faster than sequential processing
|
||||
expect(duration).toBeLessThan(15000);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
#### 4.3 Coverage Analysis Integration
|
||||
**Priority**: Low
|
||||
**Impact**: Code quality visibility
|
||||
|
||||
**Proposed Actions:**
|
||||
```json
|
||||
// package.json additions
|
||||
{
|
||||
"scripts": {
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"test:coverage:report": "vitest run --coverage && open coverage/index.html",
|
||||
"test:watch:coverage": "vitest --coverage --watch"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Architecture Enhancements
|
||||
|
||||
#### 5.1 Service Discovery System
|
||||
**Priority**: Low
|
||||
**Impact**: System flexibility and scalability
|
||||
|
||||
**Proposed Actions:**
|
||||
```typescript
|
||||
// Service discovery registry
|
||||
class ServiceRegistry {
|
||||
private services = new Map<string, ServiceInfo>();
|
||||
|
||||
register(service: ServiceInfo) {
|
||||
this.services.set(service.name, service);
|
||||
this.announceService(service);
|
||||
}
|
||||
|
||||
discover(serviceName: string): ServiceInfo | null {
|
||||
return this.services.get(serviceName) || null;
|
||||
}
|
||||
|
||||
private announceService(service: ServiceInfo) {
|
||||
// Announce service availability to other components
|
||||
this.broadcast('service:registered', service);
|
||||
}
|
||||
}
|
||||
|
||||
interface ServiceInfo {
|
||||
name: string;
|
||||
version: string;
|
||||
endpoint: string;
|
||||
capabilities: string[];
|
||||
healthCheck: () => Promise<boolean>;
|
||||
}
|
||||
```
|
||||
|
||||
#### 5.2 Circuit Breaker Pattern
|
||||
**Priority**: Low
|
||||
**Impact**: System resilience and fault tolerance
|
||||
|
||||
**Proposed Actions:**
|
||||
```typescript
|
||||
// Circuit breaker implementation
|
||||
class CircuitBreaker {
|
||||
private failures = 0;
|
||||
private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
|
||||
private lastFailureTime = 0;
|
||||
|
||||
async execute<T>(operation: () => Promise<T>): Promise<T> {
|
||||
if (this.state === 'OPEN') {
|
||||
if (Date.now() - this.lastFailureTime > this.timeout) {
|
||||
this.state = 'HALF_OPEN';
|
||||
} else {
|
||||
throw new Error('Circuit breaker is OPEN');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await operation();
|
||||
this.onSuccess();
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.onFailure();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private onSuccess() {
|
||||
this.failures = 0;
|
||||
this.state = 'CLOSED';
|
||||
}
|
||||
|
||||
private onFailure() {
|
||||
this.failures++;
|
||||
this.lastFailureTime = Date.now();
|
||||
|
||||
if (this.failures >= this.threshold) {
|
||||
this.state = 'OPEN';
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 5.3 Metrics Collection System
|
||||
**Priority**: Low
|
||||
**Impact**: Operational visibility and performance monitoring
|
||||
|
||||
**Proposed Actions:**
|
||||
```typescript
|
||||
// Metrics collection
|
||||
class MetricsCollector {
|
||||
private metrics = new Map<string, Metric>();
|
||||
|
||||
increment(name: string, value = 1, tags?: Record<string, string>) {
|
||||
const metric = this.getOrCreateMetric(name, 'counter');
|
||||
metric.value += value;
|
||||
metric.tags = tags;
|
||||
}
|
||||
|
||||
histogram(name: string, value: number, tags?: Record<string, string>) {
|
||||
const metric = this.getOrCreateMetric(name, 'histogram');
|
||||
metric.values.push(value);
|
||||
metric.tags = tags;
|
||||
}
|
||||
|
||||
gauge(name: string, value: number, tags?: Record<string, string>) {
|
||||
const metric = this.getOrCreateMetric(name, 'gauge');
|
||||
metric.value = value;
|
||||
metric.tags = tags;
|
||||
}
|
||||
|
||||
getMetrics(): MetricSnapshot[] {
|
||||
return Array.from(this.metrics.values()).map(metric => ({
|
||||
...metric,
|
||||
timestamp: Date.now(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. Developer Experience Improvements
|
||||
|
||||
#### 6.1 Development Server with Hot Reload
|
||||
**Priority**: Low
|
||||
**Impact**: Development productivity
|
||||
|
||||
**Proposed Actions:**
|
||||
```typescript
|
||||
// Development server
|
||||
class DevServer {
|
||||
private watchers: Array<() => void> = [];
|
||||
|
||||
start() {
|
||||
// Watch TypeScript files
|
||||
this.watch('src/**/*.ts', () => {
|
||||
this.rebuild();
|
||||
this.restartServices();
|
||||
});
|
||||
|
||||
// Watch configuration files
|
||||
this.watch('config.json', () => {
|
||||
this.reloadConfig();
|
||||
});
|
||||
}
|
||||
|
||||
private async rebuild() {
|
||||
console.log('🔨 Rebuilding...');
|
||||
await this.run('npm run build');
|
||||
console.log('✅ Build complete');
|
||||
}
|
||||
|
||||
private async restartServices() {
|
||||
console.log('🔄 Restarting services...');
|
||||
// Restart only changed services
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 6.2 Enhanced Debugging Tools
|
||||
**Priority**: Low
|
||||
**Impact**: Debugging efficiency
|
||||
|
||||
**Proposed Actions:**
|
||||
```typescript
|
||||
// Debugging utilities
|
||||
class DebugTools {
|
||||
static async traceMessageFlow(messageId: string) {
|
||||
const trace = await this.collectTrace(messageId);
|
||||
console.table(trace);
|
||||
}
|
||||
|
||||
static async inspectService(serviceName: string) {
|
||||
const status = await this.getServiceStatus(serviceName);
|
||||
const metrics = await this.getServiceMetrics(serviceName);
|
||||
|
||||
return { status, metrics };
|
||||
}
|
||||
|
||||
static async simulateFailure(serviceName: string, errorType: string) {
|
||||
// Controlled failure simulation for testing
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 6.3 CLI Management Commands
|
||||
**Priority**: Low
|
||||
**Impact**: Operational efficiency
|
||||
|
||||
**Proposed Actions:**
|
||||
```bash
|
||||
# CLI commands to implement
|
||||
manbot start # Start all services
|
||||
manbot stop # Stop all services
|
||||
manbot status # Show service status
|
||||
manbot logs [service] # Show service logs
|
||||
manbot config validate # Validate configuration
|
||||
manbot config show # Show current configuration
|
||||
manbot test e2e # Run end-to-end tests
|
||||
manbot benchmark # Run performance benchmarks
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Roadmap
|
||||
|
||||
### Phase 1: High Priority (1-2 weeks)
|
||||
1. **Type Safety Improvements**
|
||||
- Audit and replace `any` types
|
||||
- Create comprehensive interfaces
|
||||
- Implement strict error types
|
||||
|
||||
2. **Import Path Optimization**
|
||||
- Configure path mapping
|
||||
- Create barrel exports
|
||||
- Replace relative imports
|
||||
|
||||
3. **Security Audit**
|
||||
- Run dependency audit
|
||||
- Update vulnerable packages
|
||||
- Implement security scanning
|
||||
|
||||
### Phase 2: Medium Priority (2-4 weeks)
|
||||
1. **Performance Optimizations**
|
||||
- Browser service pooling
|
||||
- Smart model management
|
||||
- Async pattern optimization
|
||||
|
||||
2. **Testing Enhancement**
|
||||
- E2E testing framework
|
||||
- Performance benchmarks
|
||||
- Coverage analysis
|
||||
|
||||
3. **Error Handling Standardization**
|
||||
- Centralized error types
|
||||
- Consistent error patterns
|
||||
- Enhanced error context
|
||||
|
||||
### Phase 3: Low Priority (4-8 weeks)
|
||||
1. **Architecture Enhancements**
|
||||
- Service discovery
|
||||
- Circuit breakers
|
||||
- Metrics collection
|
||||
|
||||
2. **Developer Experience**
|
||||
- Development server
|
||||
- Debugging tools
|
||||
- CLI commands
|
||||
|
||||
3. **Documentation & Maintenance**
|
||||
- API documentation
|
||||
- Architecture diagrams
|
||||
- Maintenance guides
|
||||
|
||||
---
|
||||
|
||||
## Success Metrics
|
||||
|
||||
### Code Quality Metrics
|
||||
- **Type Safety**: Reduce `any` usage by 90%
|
||||
- **Import Complexity**: Reduce relative imports by 80%
|
||||
- **Test Coverage**: Achieve 85%+ coverage
|
||||
- **Error Handling**: Standardize error patterns across all services
|
||||
|
||||
### Performance Metrics
|
||||
- **Response Time**: 20% improvement in average task completion
|
||||
- **Memory Usage**: 30% reduction in browser service memory
|
||||
- **Concurrency**: Handle 10x more concurrent requests
|
||||
- **Uptime**: 99.9% service availability
|
||||
|
||||
### Developer Experience Metrics
|
||||
- **Setup Time**: Reduce new developer setup to <15 minutes
|
||||
- **Debug Time**: 50% reduction in debugging time
|
||||
- **Build Time**: <5 second incremental builds
|
||||
- **Documentation**: 100% API coverage
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
The ManBot platform demonstrates excellent architectural foundation with sophisticated multi-agent capabilities. The proposed improvements focus on enhancing type safety, performance optimization, security hardening, and developer experience while maintaining the platform's core strengths.
|
||||
|
||||
Implementing these improvements incrementally will ensure continued platform evolution while maintaining system stability and reliability. The phased approach allows for careful testing and validation of each enhancement before full deployment.
|
||||
|
||||
The improvements outlined in this document provide a clear roadmap for elevating ManBot from a functional prototype to a production-ready, enterprise-grade AI platform.
|
||||
1419
package-lock.json
generated
1419
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import TelegramBot from "node-telegram-bot-api";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { mkdir, createWriteStream } from "node:fs";
|
||||
import { mkdir, createWriteStream, createReadStream, existsSync } from "node:fs";
|
||||
import { pipeline } from "node:stream/promises";
|
||||
import { resolve, extname } from "node:path";
|
||||
import { PROTOCOL_VERSION } from "../shared/protocol.js";
|
||||
@@ -126,7 +126,7 @@ const HELP_TEXT = `Commands:
|
||||
- Cancel a reminder: /cancel_reminder <id>
|
||||
|
||||
🛠 Skills:
|
||||
- Add a new skill: /add_skill <URL_TO_SKILL_MD> (downloads to an underscore-prefixed folder)`;
|
||||
- Add a new skill: /add_skill <URL_TO_SKILL_MD> (git-ignored by default)`;
|
||||
|
||||
/** chatId -> current conversation ID for session grouping */
|
||||
const conversationIdByChat = new Map<number, string>();
|
||||
@@ -476,7 +476,7 @@ function main(): void {
|
||||
createWriteStream(targetPath)
|
||||
);
|
||||
|
||||
await sendToUser(chatId, `✅ Skill added to folder: <code>${folderName}</code>\n\nNotes:\n- The skill is currently <b>disabled</b> (starts with <code>_</code>).\n- Rename the folder to remove the underscore to enable it.\n- Use <code>/help</code> to see available commands.`, undefined, undefined, true);
|
||||
await sendToUser(chatId, `✅ Skill added and <b>enabled</b> in folder: <code>${folderName}</code>\n\nNotes:\n- The folder starts with <code>_</code> to keep it out of Git (ignored).\n- Use <code>/help</code> to see available commands.`, undefined, undefined, true);
|
||||
} catch (err) {
|
||||
console.error("[telegram-adapter] /add_skill error:", err);
|
||||
await sendToUser(chatId, `❌ Error adding skill: ${err instanceof Error ? err.message : String(err)}`);
|
||||
@@ -508,34 +508,52 @@ function main(): void {
|
||||
if (typeof pl.chatId === "number" && typeof pl.localPath === "string") {
|
||||
(async () => {
|
||||
try {
|
||||
const path = pl.localPath;
|
||||
const caption = pl.caption;
|
||||
const type = pl.type;
|
||||
const { localPath: path, caption, type } = pl;
|
||||
|
||||
if (!existsSync(path) && !path.startsWith("http")) {
|
||||
throw new Error(`File not found at path: ${path}`);
|
||||
}
|
||||
|
||||
const sendOptions: any = {
|
||||
caption: caption ? escapeHtml(caption) : undefined,
|
||||
parse_mode: "HTML"
|
||||
};
|
||||
|
||||
// Use Stream for local files to ensure multipart/form-data upload
|
||||
const fileData = (path.startsWith("http://") || path.startsWith("https://"))
|
||||
? path
|
||||
: createReadStream(path);
|
||||
|
||||
if (type === "photo") {
|
||||
await bot.sendPhoto(pl.chatId, path, { caption });
|
||||
await bot.sendPhoto(pl.chatId, fileData, sendOptions);
|
||||
} else if (type === "audio") {
|
||||
await bot.sendAudio(pl.chatId, path, { caption });
|
||||
await bot.sendAudio(pl.chatId, fileData, sendOptions);
|
||||
} else if (type === "video") {
|
||||
await bot.sendVideo(pl.chatId, path, { caption });
|
||||
await bot.sendVideo(pl.chatId, fileData, sendOptions);
|
||||
} else if (type === "document") {
|
||||
await bot.sendDocument(pl.chatId, path, { caption });
|
||||
await bot.sendDocument(pl.chatId, fileData, sendOptions);
|
||||
} else {
|
||||
// Auto-detect based on extension if not provided
|
||||
const ext = extname(path).toLowerCase();
|
||||
const ext = (extname(path).toLowerCase().split('?')[0]) ?? "";
|
||||
if ([".jpg", ".jpeg", ".png", ".gif", ".bmp"].includes(ext)) {
|
||||
await bot.sendPhoto(pl.chatId, path, { caption });
|
||||
await bot.sendPhoto(pl.chatId, fileData, sendOptions);
|
||||
} else if ([".mp3", ".wav", ".m4a", ".ogg"].includes(ext)) {
|
||||
await bot.sendAudio(pl.chatId, path, { caption });
|
||||
await bot.sendAudio(pl.chatId, fileData, sendOptions);
|
||||
} else if ([".mp4", ".mov", ".avi", ".mkv"].includes(ext)) {
|
||||
await bot.sendVideo(pl.chatId, path, { caption });
|
||||
await bot.sendVideo(pl.chatId, fileData, sendOptions);
|
||||
} else {
|
||||
await bot.sendDocument(pl.chatId, path, { caption });
|
||||
await bot.sendDocument(pl.chatId, fileData, sendOptions);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
console.error("[telegram-adapter] Error sending file:", err);
|
||||
sendToUser(pl.chatId, `⚠️ Failed to send file: ${err instanceof Error ? err.message : String(err)}`);
|
||||
let userMsg = err instanceof Error ? err.message : String(err);
|
||||
if (userMsg.includes("wrong type of the web page content")) {
|
||||
userMsg = "Telegram could not fetch this URL as a file. Make sure it's a direct link to binary data, or download it to the sandbox first.";
|
||||
} else if (userMsg.includes("FILE_PART_0_MISSING")) {
|
||||
userMsg = "Failed to upload file to Telegram. The file might be empty or inaccessible.";
|
||||
}
|
||||
sendToUser(pl.chatId, `⚠️ Failed to send file: ${userMsg}`);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
@@ -64,17 +64,32 @@ const SKILL_TOOLS: any[] = [
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
local_file_url: { type: "string", description: "Absolute path to the file in the sandbox" },
|
||||
local_path: { type: "string", description: "Absolute path to the file in the sandbox. MUST be a local path, not a URL." },
|
||||
brief_file_description: { type: "string", description: "Brief description or caption for the file" },
|
||||
chatId: { type: "number", description: "Optional chat ID. If omitted, will use the current chat context." }
|
||||
},
|
||||
required: ["local_file_url"]
|
||||
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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { resolve, isAbsolute } from "node:path";
|
||||
import { BaseProcess } from "../shared/base-process.js";
|
||||
import type { CapabilityGraph, CapabilityNode } from "../shared/graph-utils.js";
|
||||
import {
|
||||
@@ -88,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";
|
||||
@@ -101,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",
|
||||
@@ -481,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)) {
|
||||
@@ -497,26 +515,39 @@ export class ExecutorAgent extends BaseProcess {
|
||||
}
|
||||
}
|
||||
|
||||
const skillPrompt = this.skillManager.getSkillPrompt(skillName);
|
||||
if (!skillPrompt) {
|
||||
reject(new Error(`Skill prompt not found: ${skillName}`));
|
||||
return;
|
||||
}
|
||||
|
||||
// Active Skill loop
|
||||
// Active Agent/Skill loop
|
||||
(async () => {
|
||||
try {
|
||||
const messages: any[] = [
|
||||
{ role: "system", content: skillPrompt },
|
||||
{ role: "user", content: task }
|
||||
];
|
||||
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;
|
||||
}
|
||||
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
|
||||
},
|
||||
@@ -531,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) { }
|
||||
}
|
||||
const toolResult = await this.callTool(taskId, node.id, tc.function.name, args, context);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -859,8 +896,11 @@ export class ExecutorAgent extends BaseProcess {
|
||||
): Promise<unknown> {
|
||||
const input = node.input ?? {};
|
||||
const nodeInput = input as Record<string, unknown>;
|
||||
|
||||
let localPath = (nodeInput.local_file_url as string) || (nodeInput.path as string);
|
||||
|
||||
let localPath = (nodeInput.local_path as string) || (nodeInput.local_file_url as string) || (nodeInput.path as string);
|
||||
if (localPath && (localPath.startsWith("http://") || localPath.startsWith("https://"))) {
|
||||
throw new Error(`send_file requires a local path, but received a URL: ${localPath}. Download the file first using shell (curl/wget) if needed.`);
|
||||
}
|
||||
let caption = (nodeInput.brief_file_description as string) || (nodeInput.caption as string);
|
||||
const chatId = (nodeInput.chatId as number) || (context._chatId as number);
|
||||
|
||||
@@ -899,8 +939,13 @@ export class ExecutorAgent extends BaseProcess {
|
||||
if (localPath) localPath = resolveValue(localPath).trim();
|
||||
if (caption) caption = resolveValue(caption).trim();
|
||||
|
||||
// Resolve relative path against sandboxDir
|
||||
if (localPath && !isAbsolute(localPath) && !localPath.startsWith("http")) {
|
||||
localPath = resolve(getConfig().toolHost.sandboxDir, localPath);
|
||||
}
|
||||
|
||||
if (!localPath) {
|
||||
throw new Error("send_file requires local_file_url");
|
||||
throw new Error("send_file requires local_path (or local_file_url)");
|
||||
}
|
||||
if (!chatId) {
|
||||
throw new Error("send_file requires chatId");
|
||||
|
||||
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 3 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_file_url\`, \`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."
|
||||
"input": {
|
||||
"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}}"
|
||||
"input": {
|
||||
"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_file_url": "{{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>
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
export const TELEGRAM_HTML_FORMAT_INSTRUCTION = `## TELEGRAM HTML FORMATTING RULES:
|
||||
You MUST format your output using Telegram-supported HTML tags. Do NOT use Markdown syntax.
|
||||
|
||||
1. **No Markdown**: Do NOT use *, **, _, ~~, \`, #, or any Markdown syntax. Use HTML tags only.
|
||||
2. **No Tables**: HTML tables are not supported by Telegram. Use structured bullet points (•) or bold lists.
|
||||
3. **Supported HTML tags**:
|
||||
1. NO Markdown: Do NOT use *, **, _, ~~, \`, #, or any Markdown syntax. Use HTML tags only.
|
||||
2. NO Tables: HTML tables are not supported by Telegram. Use structured bullet points (•) or bold lists.
|
||||
3. Supported HTML tags:
|
||||
- Bold: <b>text</b>
|
||||
- Italic: <i>text</i>
|
||||
- Underline: <u>text</u>
|
||||
@@ -21,8 +21,8 @@ You MUST format your output using Telegram-supported HTML tags. Do NOT use Markd
|
||||
- Block quote: <blockquote>quote</blockquote>
|
||||
- Expandable Block quote (for long quotes): <blockquote expandable>quote</blockquote>
|
||||
- Code block with language: <pre><code class="language-python">code</code></pre>
|
||||
4. **Special characters**: The characters <, > and & must be replaced with <, > and & respectively when used as literals (not as part of HTML tags).
|
||||
5. **Line breaks**: Use regular line breaks (newlines). Do NOT use <br> tags.`;
|
||||
4. Special characters: The characters <, > and & must be replaced with <, > and & respectively when used as literals (not as part of HTML tags).
|
||||
5. Line breaks: Use regular line breaks (newlines). Do NOT use <br> tags.`;
|
||||
|
||||
/**
|
||||
* Default system prompt for LLM calls that need Telegram HTML formatting
|
||||
|
||||
@@ -191,22 +191,28 @@ export class Orchestrator {
|
||||
return;
|
||||
}
|
||||
// Handle cron AI query events from cron-manager
|
||||
if (fromProcess === "cron-manager" && envelope.type === "event.cron.ai_query") {
|
||||
this.handleCronAIQueryEvent(envelope);
|
||||
return;
|
||||
}
|
||||
if (fromProcess === "cron-manager") {
|
||||
if (envelope.type === "event.cron.ai_query") {
|
||||
this.handleCronAIQueryEvent(envelope);
|
||||
} else if (envelope.type === "event.cron.completed") {
|
||||
const pl = envelope.payload as Record<string, unknown>;
|
||||
if (pl.taskType === "reminder") {
|
||||
this.handleCronReminderEvent(envelope);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle cron reminder events from cron-manager
|
||||
if (fromProcess === "cron-manager" && envelope.type === "event.cron.completed") {
|
||||
this.handleCronReminderEvent(envelope);
|
||||
// Also forward to logger as before
|
||||
// Always forward cron events to logger for audit trail
|
||||
const logger = this.children.get("logger");
|
||||
if (logger?.stdin.writable) {
|
||||
logger.stdin.write(trimmed + "\n");
|
||||
ConsoleLogger.ipc("core", "→", envelope);
|
||||
this.broadcastIpcLog("→", "core", "logger", envelope);
|
||||
}
|
||||
return;
|
||||
|
||||
// If it was one of our handled events, we're done
|
||||
if (envelope.type === "event.cron.ai_query" || envelope.type === "event.cron.completed") {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (to === "core") {
|
||||
@@ -559,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;
|
||||
@@ -570,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;
|
||||
@@ -580,7 +592,7 @@ export class Orchestrator {
|
||||
previousPlan = plan;
|
||||
const nodes = plan.nodes as Array<{ id: string; type: string; service: string; input?: unknown }>;
|
||||
const edges = (plan.edges ?? []) as Array<{ from: string; to: string }>;
|
||||
|
||||
|
||||
// Update task DAG in memory
|
||||
await this.sendAndWait(taskMemory, "task.updateDag", {
|
||||
taskId,
|
||||
@@ -592,7 +604,7 @@ export class Orchestrator {
|
||||
}).catch(() => { });
|
||||
|
||||
this.sendToTelegram(chatId, "💨 Planning complete. Execution started...", true);
|
||||
|
||||
|
||||
// Explicitly set task to 'running' before dispatching to executor
|
||||
await this.sendAndWait(taskMemory, "task.updateStatus", { taskId, status: "running" }).catch(() => { });
|
||||
|
||||
@@ -610,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;
|
||||
@@ -676,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);
|
||||
@@ -793,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);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -999,6 +1038,9 @@ export class Orchestrator {
|
||||
|
||||
ConsoleLogger.info("core", `Triggering autonomous AI task: "${query}" for chatId ${chatIdNum} (taskId: ${taskId})`);
|
||||
|
||||
// TODO: Immediate feedback so user knows the cron triggered
|
||||
// this.sendToTelegram(chatIdNum, `🤖 <b>Autonomous task triggered</b>:\n<blockquote>${query}</blockquote>\n\nStarting planning...`, true, "HTML");
|
||||
|
||||
// Route to task queue (priority 0 for synthetic)
|
||||
this.enqueueTask({
|
||||
chatId: chatIdNum,
|
||||
@@ -1028,30 +1070,48 @@ export class Orchestrator {
|
||||
id: string;
|
||||
cronExpr: string;
|
||||
taskType: string;
|
||||
payload: string;
|
||||
enabled: boolean;
|
||||
}>;
|
||||
|
||||
ConsoleLogger.info("core", `Found ${schedules.length} total schedules`);
|
||||
|
||||
// Filter reminders for this chatId - we need to check the payload of each schedule
|
||||
// Since we can't easily query by chatId, we'll need to get schedule details
|
||||
// For now, filter by taskType === "reminder"
|
||||
const reminderSchedules = schedules.filter((s) => s.taskType === "reminder" && s.enabled);
|
||||
// Filter reminders for this chatId
|
||||
const filteredSchedules = schedules.filter((s) => {
|
||||
if (!s.enabled) return false;
|
||||
if (s.taskType !== "reminder" && s.taskType !== "ai_query") return false;
|
||||
|
||||
ConsoleLogger.info("core", `Found ${reminderSchedules.length} reminder schedules`);
|
||||
try {
|
||||
const payload = JSON.parse(s.payload);
|
||||
return payload.chatId === chatId;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (reminderSchedules.length === 0) {
|
||||
ConsoleLogger.info("core", `Found ${filteredSchedules.length} filtered schedules for chatId ${chatId}`);
|
||||
|
||||
if (filteredSchedules.length === 0) {
|
||||
ConsoleLogger.info("core", "No reminders found, sending 'No active reminders' message");
|
||||
this.sendToTelegram(chatId, "👌🏻 No active reminders.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Format reminders - we'll show ID and cronExpr, but reminderMessage is in the payload
|
||||
// For a better implementation, we'd need to query each schedule's payload
|
||||
const formatted = reminderSchedules
|
||||
.map((rem) => `ID: ${rem.id}\nTime: ${rem.cronExpr}`)
|
||||
// Format reminders - include message/query and task type
|
||||
const formatted = filteredSchedules
|
||||
.map((s) => {
|
||||
let message = "N/A";
|
||||
try {
|
||||
const p = JSON.parse(s.payload);
|
||||
message = p.reminderMessage || p.query || "N/A";
|
||||
} catch (e) { }
|
||||
|
||||
const typeLabel = s.taskType === "ai_query" ? "🤖 Task" : "🔔 Reminder";
|
||||
return `<b>${typeLabel}</b>\nID: <code>${s.id}</code>\nTime: <code>${s.cronExpr}</code>\n<blockquote>Message: ${message.substring(0, 100)}...</blockquote>`;
|
||||
})
|
||||
.join("\n\n---\n\n");
|
||||
const message = `Active reminders:\n\n${formatted}`;
|
||||
|
||||
const message = `⏰ <b>Active schedules:</b>\n\n${formatted}`;
|
||||
ConsoleLogger.info("core", `Sending reminder list to chatId ${chatId}: ${message.substring(0, 100)}...`);
|
||||
this.sendToTelegram(chatId, message, false, "HTML");
|
||||
} catch (err) {
|
||||
@@ -1069,12 +1129,37 @@ export class Orchestrator {
|
||||
}
|
||||
|
||||
try {
|
||||
// Security: verify that this reminder belongs to this chatId
|
||||
const listResponse = await this.sendAndWait(cronManager, "cron.schedule.list", {});
|
||||
const listPayload = listResponse.payload as { result?: { schedules?: Array<{ id: string; payload: string }> } };
|
||||
const schedules = listPayload.result?.schedules ?? [];
|
||||
const targetSchedule = schedules.find(s => s.id === reminderId);
|
||||
|
||||
if (!targetSchedule) {
|
||||
this.sendToTelegram(chatId, `😨 Reminder <code>${reminderId}</code> not found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = JSON.parse(targetSchedule.payload);
|
||||
if (payload.chatId !== chatId) {
|
||||
this.sendToTelegram(chatId, `❌ You are not authorized to cancel this reminder.`);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
// If payload is malformed or missing chatId, we might want to prevent deletion or allow it?
|
||||
// Let's be conservative: if we can't confirm ownership, we reject.
|
||||
// UNLESS we are superuser (could add later).
|
||||
this.sendToTelegram(chatId, `❌ Security check failed for reminder <code>${reminderId}</code>.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await this.sendAndWait(cronManager, "cron.schedule.remove", { id: reminderId });
|
||||
const responsePayload = response.payload as { status?: string; result?: { removed?: string } };
|
||||
if (responsePayload.result?.removed === reminderId) {
|
||||
this.sendToTelegram(chatId, `🟢 Reminder ${reminderId} has been canceled.`);
|
||||
this.sendToTelegram(chatId, `🟢 Reminder <code>${reminderId}</code> has been canceled.`);
|
||||
} else {
|
||||
this.sendToTelegram(chatId, `😨 Reminder ${reminderId} not found.`);
|
||||
this.sendToTelegram(chatId, `😨 Reminder <code>${reminderId}</code> not found.`);
|
||||
}
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
|
||||
@@ -72,27 +72,15 @@ export class CronManager extends BaseProcess {
|
||||
|
||||
private runJob(row: ScheduleRow): void {
|
||||
const now = Date.now();
|
||||
this.emitEvent("event.cron.started", { scheduleId: row.id, taskType: row.task_type, timestamp: now });
|
||||
this.emitEvent("event.cron.started", {
|
||||
scheduleId: row.id,
|
||||
taskType: row.task_type,
|
||||
timestamp: now,
|
||||
});
|
||||
|
||||
try {
|
||||
const payload = row.payload ? JSON.parse(row.payload) : {};
|
||||
|
||||
// AI Query task type
|
||||
if (row.task_type === "ai_query") {
|
||||
const query = payload.reminderMessage || payload.query || "";
|
||||
const chatId = payload.chatId;
|
||||
const userId = payload.userId;
|
||||
|
||||
this.emitEvent("event.cron.ai_query", {
|
||||
scheduleId: row.id,
|
||||
taskType: row.task_type,
|
||||
query,
|
||||
chatId,
|
||||
userId,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Default: Reminder or generic task payload
|
||||
const reminderPayload: Record<string, unknown> = {
|
||||
scheduleId: row.id,
|
||||
@@ -101,13 +89,25 @@ export class CronManager extends BaseProcess {
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
// If this is a reminder task, extract structured fields
|
||||
if (row.task_type === "reminder" || payload.chatId || payload.reminderMessage) {
|
||||
reminderPayload.chatId = payload.chatId;
|
||||
reminderPayload.reminderMessage = payload.reminderMessage;
|
||||
reminderPayload.userId = payload.userId;
|
||||
// Extract common fields for easier routing in Orchestrator if present
|
||||
const q = payload.reminderMessage || payload.query;
|
||||
if (q) reminderPayload.reminderMessage = q;
|
||||
if (payload.chatId !== undefined) reminderPayload.chatId = payload.chatId;
|
||||
if (payload.userId !== undefined) reminderPayload.userId = payload.userId;
|
||||
|
||||
// Emit specific event types for legacy/orchestrator compatibility
|
||||
if (row.task_type === "ai_query") {
|
||||
this.emitEvent("event.cron.ai_query", {
|
||||
scheduleId: row.id,
|
||||
taskType: row.task_type,
|
||||
query: payload.reminderMessage || payload.query || "",
|
||||
chatId: payload.chatId,
|
||||
userId: payload.userId,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
// Always emit completed event for tracking and logging
|
||||
this.emitEvent("event.cron.completed", reminderPayload);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
@@ -148,17 +148,19 @@ export class CronManager extends BaseProcess {
|
||||
return id;
|
||||
}
|
||||
|
||||
private listSchedules(): Array<{ id: string; cronExpr: string; taskType: string; enabled: boolean }> {
|
||||
const rows = this.db.prepare("SELECT id, cron_expr, task_type, enabled FROM cron_schedules").all() as Array<{
|
||||
private listSchedules(): Array<{ id: string; cronExpr: string; taskType: string; payload: string; enabled: boolean }> {
|
||||
const rows = this.db.prepare("SELECT id, cron_expr, task_type, payload, enabled FROM cron_schedules").all() as Array<{
|
||||
id: string;
|
||||
cron_expr: string;
|
||||
task_type: string;
|
||||
payload: string;
|
||||
enabled: number;
|
||||
}>;
|
||||
return rows.map((r) => ({
|
||||
id: r.id,
|
||||
cronExpr: r.cron_expr,
|
||||
taskType: r.task_type,
|
||||
payload: r.payload,
|
||||
enabled: r.enabled === 1,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -252,7 +252,7 @@ export class DashboardService extends BaseProcess {
|
||||
if (fs.existsSync(logPath)) {
|
||||
stats.logs = fs.readFileSync(logPath, 'utf8').trim().split('\n').map(line => {
|
||||
try { return JSON.parse(line); } catch (e) { return { message: line }; }
|
||||
}).reverse();
|
||||
}).filter((l: any) => l.type !== "event.system.heartbeat").reverse();
|
||||
}
|
||||
} catch (e) { }
|
||||
return stats;
|
||||
|
||||
@@ -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;
|
||||
const skillName = input.skillName || input.skill;
|
||||
if (skillName) chipAttr = ` data-title="Skill: ${skillName}"`;
|
||||
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>`;
|
||||
|
||||
@@ -57,7 +57,7 @@ export class LoggerService extends BaseProcess {
|
||||
}
|
||||
|
||||
protected override handleEnvelope(envelope: Envelope): void {
|
||||
if (!envelope.type.startsWith("event.")) {
|
||||
if (!envelope.type.startsWith("event.") || envelope.type === "event.system.heartbeat") {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,8 +31,8 @@ export class SkillManager {
|
||||
const skills: SkillInfo[] = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
// Ignore hidden directories and those starting with underscore (disabled)
|
||||
if (entry.isDirectory() && !entry.name.startsWith(".") && !entry.name.startsWith("_")) {
|
||||
// Ignore hidden directories
|
||||
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
||||
const skillMdPath = join(this.skillsDir, entry.name, "SKILL.md");
|
||||
if (existsSync(skillMdPath)) {
|
||||
try {
|
||||
@@ -40,12 +40,12 @@ export class SkillManager {
|
||||
const lines = content.split("\n").filter(l => l.trim() !== "");
|
||||
const firstLine = lines[0]?.trim();
|
||||
if (!firstLine) continue;
|
||||
|
||||
|
||||
let description = firstLine;
|
||||
if (firstLine.toLowerCase().startsWith("description:")) {
|
||||
description = firstLine.substring("description:".length).trim();
|
||||
}
|
||||
|
||||
|
||||
if (description) {
|
||||
skills.push({
|
||||
name: entry.name,
|
||||
@@ -76,7 +76,7 @@ export class SkillManager {
|
||||
|
||||
try {
|
||||
const content = readFileSync(skillPath, "utf-8");
|
||||
return `${content}\n\n## OUTPUT FORMATTING\n${TELEGRAM_HTML_FORMAT_INSTRUCTION}\n\nYou MUST format your final response using only the Telegram HTML tags listed above. Never use Markdown (replace will allowed HTML tags).`;
|
||||
return `${content}\n\n## OUTPUT FORMATTING\nYou MUST format your final response using only the Telegram HTML tags listed above. Never use Markdown (replace will allowed HTML tags).\n\n${TELEGRAM_HTML_FORMAT_INSTRUCTION}`;
|
||||
} catch (err) {
|
||||
console.error(`Failed to load skill prompt for ${name}:`, err);
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user