@saber2pr/ai-agent 0.0.6 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -43
- package/lib/agent-chain.d.ts +48 -0
- package/lib/agent-chain.js +315 -0
- package/lib/agent.d.ts +15 -0
- package/lib/agent.js +61 -6
- package/lib/cli-chain.d.ts +2 -0
- package/lib/cli-chain.js +9 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +3 -1
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -1,74 +1,141 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 🚀 Saber2pr AI Agent
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A high-performance AI Agent toolkit designed for automated code auditing, repository mapping, and architectural analysis. It supports both a lightweight **Standard Edition** for direct API interaction and a powerful **LangChain Edition** for complex multi-step reasoning and private LLM integration.
|
|
4
4
|
|
|
5
|
-
## ✨ Features
|
|
5
|
+
## ✨ Core Features
|
|
6
6
|
|
|
7
|
-
* **
|
|
8
|
-
* **
|
|
9
|
-
* **
|
|
10
|
-
* **Namespace Management**: Prevents tool name conflicts by automatically prefixing functions (e.g., `serverName__toolName`).
|
|
11
|
-
* **Interactive CLI**: Built-in REPL for multi-turn conversations and complex tool-chaining.
|
|
7
|
+
* **Dual Mode Support**:
|
|
8
|
+
* **Standard Mode**: Lightweight, fast, and uses direct OpenAI-compatible API calls.
|
|
9
|
+
* **LangChain Mode**: Orchestrated via ReAct agents, supporting complex tool-chains and custom model extensions.
|
|
12
10
|
|
|
13
|
-
## 📦 Installation
|
|
14
11
|
|
|
15
|
-
|
|
12
|
+
* **MCP Integration**: Built on the Model Context Protocol to bridge local development environments with AI.
|
|
13
|
+
* **Repository Intelligence**: Integrated `PromptEngine` for generating project maps and code skeletons without exhausting tokens.
|
|
14
|
+
* **Automated Audit Workflow**: Specialized tools for locating code violations, providing line-specific fixes, and generating structured JSON reports.
|
|
15
|
+
* **Private LLM Gateway**: Easily adapt to non-standard API protocols (e.g., Jarvis, internal enterprise gateways) by extending the `BaseChatModel`.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 🛠️ Installation
|
|
16
20
|
|
|
17
21
|
```bash
|
|
18
|
-
|
|
22
|
+
# Clone the repository
|
|
23
|
+
git clone https://github.com/saber2pr/ai-agent.git
|
|
24
|
+
cd ai-agent
|
|
25
|
+
|
|
26
|
+
# Install dependencies
|
|
27
|
+
npm install
|
|
28
|
+
|
|
29
|
+
# Build the project
|
|
30
|
+
npm run build
|
|
31
|
+
|
|
19
32
|
```
|
|
20
33
|
|
|
21
|
-
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 🚀 Usage Modes
|
|
37
|
+
|
|
38
|
+
### 1. Standard Edition (Direct API)
|
|
39
|
+
|
|
40
|
+
Best for quick scripts and simple chat interactions. It uses a straightforward message-loop logic.
|
|
41
|
+
|
|
42
|
+
```javascript
|
|
43
|
+
const McpAgent = require("./lib/agent").default;
|
|
44
|
+
|
|
45
|
+
const agent = new McpAgent({
|
|
46
|
+
targetDir: "/path/to/project"
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
await agent.chat("Analyze the directory structure.");
|
|
22
50
|
|
|
23
|
-
```bash
|
|
24
|
-
npx @saber2pr/ai-agent
|
|
25
51
|
```
|
|
26
52
|
|
|
27
|
-
|
|
53
|
+
### 2. LangChain Edition (Advanced Agent)
|
|
28
54
|
|
|
29
|
-
|
|
55
|
+
Best for complex tasks like "Audit the whole project and fix bugs." It supports autonomous tool usage.
|
|
30
56
|
|
|
31
|
-
|
|
57
|
+
```javascript
|
|
58
|
+
const McpAgent = require("./lib/agent-chain").default;
|
|
59
|
+
const { MyPrivateLLM } = require("./your-custom-llm");
|
|
60
|
+
|
|
61
|
+
const agent = new McpAgent({
|
|
62
|
+
apiModel: new MyPrivateLLM(), // Inject custom LLM
|
|
63
|
+
maxIterations: 15,
|
|
64
|
+
targetDir: "/path/to/project"
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
await agent.chat("Scan for hardcoded colors and submit a review report.");
|
|
32
68
|
|
|
33
|
-
```bash
|
|
34
|
-
sagent
|
|
35
69
|
```
|
|
36
70
|
|
|
37
|
-
|
|
71
|
+
---
|
|
38
72
|
|
|
39
|
-
|
|
73
|
+
## 🔧 Extending with Private LLMs
|
|
40
74
|
|
|
41
|
-
|
|
42
|
-
* **API Key**: Your model provider's API key.
|
|
43
|
-
* **Model Name**: e.g., `gpt-4o`, `claude-3-5-sonnet`, or `deepseek-v3`.
|
|
75
|
+
To use your own API protocol, extend the `BaseChatModel` from `@langchain/core`:
|
|
44
76
|
|
|
45
|
-
|
|
77
|
+
```javascript
|
|
78
|
+
const { BaseChatModel } = require("@langchain/core/language_models/chat_models");
|
|
79
|
+
|
|
80
|
+
class MyPrivateLLM extends BaseChatModel {
|
|
81
|
+
async _generate(messages) {
|
|
82
|
+
const lastMessage = messages[messages.length - 1];
|
|
83
|
+
const response = await fetch("https://your-api.com/v1/chat", {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
body: JSON.stringify({ query: lastMessage.content }),
|
|
86
|
+
headers: { 'Authorization': `Bearer ${token}` }
|
|
87
|
+
});
|
|
88
|
+
const data = await response.json();
|
|
89
|
+
return {
|
|
90
|
+
generations: [{ text: data.text, message: { content: data.text, role: "assistant" } }]
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
_llmType() { return "private_llm"; }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
```
|
|
46
97
|
|
|
47
|
-
|
|
98
|
+
---
|
|
48
99
|
|
|
49
|
-
|
|
100
|
+
## 📦 Built-in Toolset
|
|
50
101
|
|
|
51
|
-
|
|
52
|
-
|
|
102
|
+
| Tool | Description |
|
|
103
|
+
| ----------------- | ------------------------------------------------------------------------ |
|
|
104
|
+
| `generate_review` | Finalizes the process by submitting a structured violation report. |
|
|
105
|
+
| `get_repo_map` | Generates a high-level map of the project files and exports. |
|
|
106
|
+
| `read_full_code` | Reads file content with line numbers for precise auditing. |
|
|
107
|
+
| `read_skeleton` | Extracts class/function signatures without full logic (Token efficient). |
|
|
53
108
|
|
|
54
|
-
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## 📋 Audit Rule Configuration
|
|
112
|
+
|
|
113
|
+
You can pass structured rules via the `extraSystemPrompt`:
|
|
114
|
+
|
|
115
|
+
```javascript
|
|
116
|
+
const agent = new McpAgent({
|
|
117
|
+
extraSystemPrompt: {
|
|
118
|
+
role: "Code Auditor",
|
|
119
|
+
rules: [
|
|
120
|
+
{ id: "THEME-001", name: "Theme Check", description: "No hardcoded hex colors." }
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
```
|
|
55
126
|
|
|
56
|
-
|
|
127
|
+
---
|
|
57
128
|
|
|
58
|
-
|
|
59
|
-
| ------------------------ | ----------------------------------------------- |
|
|
60
|
-
| `~/.saber2pr-agent.json` | Manually edit this file to update API settings. |
|
|
61
|
-
| `exit` | Type during a chat to quit the program. |
|
|
62
|
-
| `sagent` | Enter interactive chat mode. |
|
|
129
|
+
## ⚙️ Configuration
|
|
63
130
|
|
|
64
|
-
|
|
131
|
+
The agent stores API keys and base URLs in `~/.saber2pr-agent.json`.
|
|
65
132
|
|
|
66
|
-
|
|
133
|
+
* `baseURL`: The API endpoint.
|
|
134
|
+
* `apiKey`: Your authentication key.
|
|
135
|
+
* `model`: The model name (e.g., `gpt-4o`, `claude-3-5-sonnet`).
|
|
67
136
|
|
|
68
|
-
|
|
69
|
-
* [openai](https://www.google.com/search?q=https://github.com/openai/openai-node) - Client for API interactions.
|
|
70
|
-
* [TypeScript](https://www.google.com/search?q=https://www.typescriptlang.org/) - Ensuring type safety and robustness.
|
|
137
|
+
---
|
|
71
138
|
|
|
72
|
-
##
|
|
139
|
+
## 📜 License
|
|
73
140
|
|
|
74
|
-
|
|
141
|
+
ISC
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { BaseChatModel } from "@langchain/core/language_models/chat_models";
|
|
2
|
+
interface ApiConfig {
|
|
3
|
+
baseURL: string;
|
|
4
|
+
apiKey: string;
|
|
5
|
+
model: string;
|
|
6
|
+
}
|
|
7
|
+
export interface CustomTool {
|
|
8
|
+
name: string;
|
|
9
|
+
description: string;
|
|
10
|
+
parameters: any;
|
|
11
|
+
handler: (args: any) => Promise<any>;
|
|
12
|
+
}
|
|
13
|
+
export interface AgentOptions {
|
|
14
|
+
targetDir?: string;
|
|
15
|
+
tools?: CustomTool[];
|
|
16
|
+
extraSystemPrompt?: any;
|
|
17
|
+
maxTokens?: number;
|
|
18
|
+
apiConfig?: ApiConfig;
|
|
19
|
+
apiModel?: BaseChatModel;
|
|
20
|
+
maxIterations?: number;
|
|
21
|
+
}
|
|
22
|
+
export default class McpChainAgent {
|
|
23
|
+
private allTools;
|
|
24
|
+
private messages;
|
|
25
|
+
private engine;
|
|
26
|
+
private encoder;
|
|
27
|
+
private extraTools;
|
|
28
|
+
private maxTokens;
|
|
29
|
+
private executor?;
|
|
30
|
+
private apiConfig;
|
|
31
|
+
private maxIterations;
|
|
32
|
+
private apiModel?;
|
|
33
|
+
constructor(options?: AgentOptions);
|
|
34
|
+
/**
|
|
35
|
+
* 工具处理器包装逻辑:增加日志打印和 Token 监控
|
|
36
|
+
*/
|
|
37
|
+
private wrapHandler;
|
|
38
|
+
private registerBuiltinTools;
|
|
39
|
+
private injectCustomTools;
|
|
40
|
+
private calculateTokens;
|
|
41
|
+
private pruneMessages;
|
|
42
|
+
init(): Promise<void>;
|
|
43
|
+
chat(input: string): Promise<string>;
|
|
44
|
+
private showLoading;
|
|
45
|
+
start(): Promise<void>;
|
|
46
|
+
private ensureApiConfig;
|
|
47
|
+
}
|
|
48
|
+
export {};
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
const fs_1 = __importDefault(require("fs"));
|
|
40
|
+
const path_1 = __importDefault(require("path"));
|
|
41
|
+
const os_1 = __importDefault(require("os"));
|
|
42
|
+
const readline = __importStar(require("readline"));
|
|
43
|
+
const ts_context_mcp_1 = require("@saber2pr/ts-context-mcp");
|
|
44
|
+
const js_tiktoken_1 = require("js-tiktoken");
|
|
45
|
+
const openai_1 = require("@langchain/openai");
|
|
46
|
+
const tools_1 = require("@langchain/core/tools");
|
|
47
|
+
const agents_1 = require("langchain/agents");
|
|
48
|
+
const prompts_1 = require("@langchain/core/prompts");
|
|
49
|
+
const CONFIG_FILE = path_1.default.join(os_1.default.homedir(), ".saber2pr-agent.json");
|
|
50
|
+
class McpChainAgent {
|
|
51
|
+
constructor(options) {
|
|
52
|
+
this.allTools = [];
|
|
53
|
+
this.messages = [];
|
|
54
|
+
this.encoder = (0, js_tiktoken_1.getEncoding)("cl100k_base");
|
|
55
|
+
this.extraTools = [];
|
|
56
|
+
this.engine = new ts_context_mcp_1.PromptEngine((options === null || options === void 0 ? void 0 : options.targetDir) || process.cwd());
|
|
57
|
+
this.extraTools = (options === null || options === void 0 ? void 0 : options.tools) || [];
|
|
58
|
+
this.maxTokens = (options === null || options === void 0 ? void 0 : options.maxTokens) || 100000;
|
|
59
|
+
this.apiConfig = options === null || options === void 0 ? void 0 : options.apiConfig;
|
|
60
|
+
this.maxIterations = (options === null || options === void 0 ? void 0 : options.maxIterations) || 20;
|
|
61
|
+
this.apiModel = options === null || options === void 0 ? void 0 : options.apiModel;
|
|
62
|
+
const baseSystemPrompt = `你是一个专业的 AI 代码架构师,具备深度的源码分析与工程化处理能力。
|
|
63
|
+
|
|
64
|
+
### 核心操作规范:
|
|
65
|
+
1. **全局扫描(强制首选)**:在开始任何分析任务前,你【必须】首先调用 'get_repo_map'。这是理解项目结构、技术栈及模块关系的唯一来源。
|
|
66
|
+
2. **循序渐进的分析路径**:
|
|
67
|
+
- 优先使用 'read_skeleton' 提取接口和函数签名。
|
|
68
|
+
- 仅在需要分析具体逻辑或准备修复代码时,才使用 'read_full_code'。
|
|
69
|
+
3. **真实性原则**:所有的代码分析必须基于工具返回的真实内容,严禁虚假猜测。`;
|
|
70
|
+
this.messages.push({
|
|
71
|
+
role: "system",
|
|
72
|
+
content: (options === null || options === void 0 ? void 0 : options.extraSystemPrompt)
|
|
73
|
+
? `${baseSystemPrompt}\n\n[额外指令]:\n${JSON.stringify(options.extraSystemPrompt)}`
|
|
74
|
+
: baseSystemPrompt,
|
|
75
|
+
});
|
|
76
|
+
this.registerBuiltinTools();
|
|
77
|
+
this.injectCustomTools();
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* 工具处理器包装逻辑:增加日志打印和 Token 监控
|
|
81
|
+
*/
|
|
82
|
+
wrapHandler(name, handler) {
|
|
83
|
+
return async (args) => {
|
|
84
|
+
// 1. 打印工具执行日志
|
|
85
|
+
console.log(`\n [工具调用]: ${name}`);
|
|
86
|
+
if (args === null || args === void 0 ? void 0 : args.filePath) {
|
|
87
|
+
console.log(` [目标文件]: ${args.filePath}`);
|
|
88
|
+
}
|
|
89
|
+
// 2. 执行逻辑
|
|
90
|
+
const result = await handler(args);
|
|
91
|
+
const content = typeof result === "string" ? result : JSON.stringify(result);
|
|
92
|
+
// 3. 统计 Token 消耗
|
|
93
|
+
const tokens = this.encoder.encode(content).length;
|
|
94
|
+
console.log(` [输出长度]: ${tokens} tokens`);
|
|
95
|
+
return content;
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
registerBuiltinTools() {
|
|
99
|
+
const builtinTools = [
|
|
100
|
+
{
|
|
101
|
+
type: "function",
|
|
102
|
+
function: { name: "get_repo_map", description: "获取项目全局结构图和导出清单", parameters: { type: "object" } },
|
|
103
|
+
_handler: this.wrapHandler("get_repo_map", async () => {
|
|
104
|
+
this.engine.refresh();
|
|
105
|
+
return this.engine.getRepoMap();
|
|
106
|
+
}),
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
type: "function",
|
|
110
|
+
function: {
|
|
111
|
+
name: "read_skeleton",
|
|
112
|
+
description: "读取代码骨架(接口、类定义等),非常节省 Token",
|
|
113
|
+
parameters: { type: "object", properties: { filePath: { type: "string" } } },
|
|
114
|
+
},
|
|
115
|
+
_handler: this.wrapHandler("read_skeleton", async ({ filePath }) => this.engine.getSkeleton(filePath)),
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
type: "function",
|
|
119
|
+
function: {
|
|
120
|
+
name: "read_full_code",
|
|
121
|
+
description: "读取完整源码。注意:仅在需要具体行号或精细逻辑时使用",
|
|
122
|
+
parameters: { type: "object", properties: { filePath: { type: "string" } } },
|
|
123
|
+
},
|
|
124
|
+
_handler: this.wrapHandler("read_full_code", async ({ filePath }) => {
|
|
125
|
+
// --- 新增:Token 守卫 ---
|
|
126
|
+
const currentTokens = this.calculateTokens();
|
|
127
|
+
if (currentTokens > this.maxTokens) {
|
|
128
|
+
return `[SYSTEM WARNING]: 当前上下文已达到 ${currentTokens} tokens (上限 ${this.maxTokens})。为了保证系统稳定,已拦截 read_full_code。请立即根据已知信息进行总结或停止阅读更多代码。`;
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
if (typeof filePath !== 'string' || !filePath) {
|
|
132
|
+
return "Error: filePath 不能为空";
|
|
133
|
+
}
|
|
134
|
+
// 拼合绝对路径
|
|
135
|
+
const fullPath = path_1.default.resolve(this.engine.getRootDir(), filePath);
|
|
136
|
+
// 安全检查:防止 AI 尝试读取项目外的敏感文件
|
|
137
|
+
if (!fullPath.startsWith(this.engine.getRootDir())) {
|
|
138
|
+
return "Error: 权限拒绝,禁止访问项目目录外的文件。";
|
|
139
|
+
}
|
|
140
|
+
if (!fs_1.default.existsSync(fullPath)) {
|
|
141
|
+
return `Error: 文件不存在: ${filePath}`;
|
|
142
|
+
}
|
|
143
|
+
const content = fs_1.default.readFileSync(fullPath, "utf-8");
|
|
144
|
+
// 加上行号,AI 就能在 generate_review 里给出准确的 line 参数
|
|
145
|
+
return content.split('\n')
|
|
146
|
+
.map((line, i) => `${i + 1} | ${line}`)
|
|
147
|
+
.join('\n');
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
return `Error: 读取文件失败: ${err.message}`;
|
|
151
|
+
}
|
|
152
|
+
}),
|
|
153
|
+
}
|
|
154
|
+
];
|
|
155
|
+
this.allTools.push(...builtinTools);
|
|
156
|
+
}
|
|
157
|
+
injectCustomTools() {
|
|
158
|
+
for (const tool of this.extraTools) {
|
|
159
|
+
this.allTools.push({
|
|
160
|
+
type: "function",
|
|
161
|
+
function: { name: tool.name, description: tool.description, parameters: tool.parameters },
|
|
162
|
+
_handler: this.wrapHandler(tool.name, tool.handler),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
calculateTokens() {
|
|
167
|
+
return this.messages.reduce((acc, msg) => acc + this.encoder.encode(String(msg.content || "")).length, 0);
|
|
168
|
+
}
|
|
169
|
+
pruneMessages() {
|
|
170
|
+
const current = this.calculateTokens();
|
|
171
|
+
if (current > this.maxTokens) {
|
|
172
|
+
console.log(`\n⚠️ 上下文达到限制 (${current} tokens),正在裁剪旧消息...`);
|
|
173
|
+
// 保留 system prompt (index 0),移除后续消息
|
|
174
|
+
while (this.calculateTokens() > this.maxTokens * 0.8 && this.messages.length > 2) {
|
|
175
|
+
this.messages.splice(1, 1);
|
|
176
|
+
}
|
|
177
|
+
console.log(`✅ 裁剪完成,当前: ${this.calculateTokens()} tokens`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async init() {
|
|
181
|
+
if (this.executor)
|
|
182
|
+
return;
|
|
183
|
+
let model;
|
|
184
|
+
if (this.apiModel) {
|
|
185
|
+
console.log("ℹ️ 使用自定义 API Model 实例");
|
|
186
|
+
model = this.apiModel;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
// 降级方案:使用配置创建默认的 ChatOpenAI
|
|
190
|
+
const apiConfig = await this.ensureApiConfig();
|
|
191
|
+
console.log(`ℹ️ 使用默认 ChatOpenAI (${apiConfig.model})`);
|
|
192
|
+
model = new openai_1.ChatOpenAI({
|
|
193
|
+
configuration: { baseURL: apiConfig.baseURL, apiKey: apiConfig.apiKey },
|
|
194
|
+
modelName: apiConfig.model,
|
|
195
|
+
temperature: 0,
|
|
196
|
+
streaming: false
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
const langchainTools = this.allTools.map(t => new tools_1.DynamicTool({
|
|
200
|
+
name: t.function.name,
|
|
201
|
+
description: t.function.description || "",
|
|
202
|
+
func: t._handler
|
|
203
|
+
}));
|
|
204
|
+
const prompt = prompts_1.PromptTemplate.fromTemplate(`
|
|
205
|
+
{system_prompt}
|
|
206
|
+
|
|
207
|
+
TOOLS:
|
|
208
|
+
------
|
|
209
|
+
You can use the following tools:
|
|
210
|
+
{tools}
|
|
211
|
+
|
|
212
|
+
To use a tool, please use the following format:
|
|
213
|
+
Thought: Do I need to use a tool? Yes
|
|
214
|
+
Action: the action to take, should be one of [{tool_names}]
|
|
215
|
+
Action Input: the input to the action (JSON format)
|
|
216
|
+
Observation: the result of the action
|
|
217
|
+
... (repeat N times)
|
|
218
|
+
Thought: I now know the final answer
|
|
219
|
+
Final Answer: the final answer to the original input question
|
|
220
|
+
|
|
221
|
+
Begin!
|
|
222
|
+
Question: {input}
|
|
223
|
+
Thought: {agent_scratchpad}`);
|
|
224
|
+
const agent = await (0, agents_1.createReactAgent)({ llm: model, tools: langchainTools, prompt });
|
|
225
|
+
this.executor = new agents_1.AgentExecutor({
|
|
226
|
+
agent,
|
|
227
|
+
tools: langchainTools,
|
|
228
|
+
verbose: false, // 我们已经有了 wrapHandler 日志,关闭原生 verbose 以保持整洁
|
|
229
|
+
handleParsingErrors: true,
|
|
230
|
+
maxIterations: this.maxIterations
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
async chat(input) {
|
|
234
|
+
var _a;
|
|
235
|
+
if (!this.executor)
|
|
236
|
+
await this.init();
|
|
237
|
+
this.messages.push({ role: "user", content: input });
|
|
238
|
+
this.pruneMessages();
|
|
239
|
+
console.log(`\n📊 状态: Context ${this.calculateTokens()} / Limit ${this.maxTokens} tokens`);
|
|
240
|
+
const stopLoading = this.showLoading("🤖 Agent 正在思考并执行工具...");
|
|
241
|
+
try {
|
|
242
|
+
const response = await this.executor.invoke({
|
|
243
|
+
input: input,
|
|
244
|
+
system_prompt: this.messages[0].content,
|
|
245
|
+
});
|
|
246
|
+
let output = response.output;
|
|
247
|
+
// 清洗 ReAct 冗余标签
|
|
248
|
+
if (output.includes("Final Answer:")) {
|
|
249
|
+
output = ((_a = output.split("Final Answer:").pop()) === null || _a === void 0 ? void 0 : _a.trim()) || output;
|
|
250
|
+
}
|
|
251
|
+
this.messages.push({ role: "assistant", content: output });
|
|
252
|
+
return output;
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
stopLoading();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
showLoading(text) {
|
|
259
|
+
const chars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
260
|
+
let i = 0;
|
|
261
|
+
const timer = setInterval(() => {
|
|
262
|
+
process.stdout.write(`\r${chars[i]} ${text}`);
|
|
263
|
+
i = (i + 1) % chars.length;
|
|
264
|
+
}, 80);
|
|
265
|
+
return () => {
|
|
266
|
+
clearInterval(timer);
|
|
267
|
+
process.stdout.write('\r\x1b[K');
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
async start() {
|
|
271
|
+
await this.init();
|
|
272
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
273
|
+
console.log(`\n🚀 AI 助手启动 (LangChain 核心)`);
|
|
274
|
+
console.log(`📂 目标目录: ${this.engine.getRootDir()}`);
|
|
275
|
+
const chatLoop = () => {
|
|
276
|
+
rl.question("\n👤 你: ", async (input) => {
|
|
277
|
+
if (!input.trim())
|
|
278
|
+
return chatLoop();
|
|
279
|
+
if (input.toLowerCase() === "exit")
|
|
280
|
+
process.exit(0);
|
|
281
|
+
try {
|
|
282
|
+
const result = await this.chat(input);
|
|
283
|
+
console.log(`\n🤖 Agent: ${result}`);
|
|
284
|
+
}
|
|
285
|
+
catch (err) {
|
|
286
|
+
console.error("\n❌ 系统错误:", err.message);
|
|
287
|
+
}
|
|
288
|
+
chatLoop();
|
|
289
|
+
});
|
|
290
|
+
};
|
|
291
|
+
chatLoop();
|
|
292
|
+
}
|
|
293
|
+
async ensureApiConfig() {
|
|
294
|
+
if (this.apiConfig)
|
|
295
|
+
return this.apiConfig;
|
|
296
|
+
if (fs_1.default.existsSync(CONFIG_FILE)) {
|
|
297
|
+
return JSON.parse(fs_1.default.readFileSync(CONFIG_FILE, "utf-8"));
|
|
298
|
+
}
|
|
299
|
+
const rl = readline.createInterface({
|
|
300
|
+
input: process.stdin,
|
|
301
|
+
output: process.stdout,
|
|
302
|
+
});
|
|
303
|
+
const question = (q) => new Promise((res) => rl.question(q, res));
|
|
304
|
+
console.log("\n🔑 配置 API 凭据:");
|
|
305
|
+
const config = {
|
|
306
|
+
baseURL: await question("? API Base URL (如 https://api.openai.com/v1): "),
|
|
307
|
+
apiKey: await question("? API Key: "),
|
|
308
|
+
model: await question("? Model Name (如 gpt-4o): "),
|
|
309
|
+
};
|
|
310
|
+
fs_1.default.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
311
|
+
rl.close();
|
|
312
|
+
return config;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
exports.default = McpChainAgent;
|
package/lib/agent.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
interface ApiConfig {
|
|
2
|
+
baseURL: string;
|
|
3
|
+
apiKey: string;
|
|
4
|
+
model: string;
|
|
5
|
+
}
|
|
1
6
|
export interface CustomTool {
|
|
2
7
|
name: string;
|
|
3
8
|
description: string;
|
|
@@ -10,6 +15,8 @@ export interface AgentOptions {
|
|
|
10
15
|
tools?: any[];
|
|
11
16
|
/** 注入到 System Prompt 中的额外指令/规则/上下文 */
|
|
12
17
|
extraSystemPrompt?: any;
|
|
18
|
+
maxTokens?: number;
|
|
19
|
+
apiConfig?: ApiConfig;
|
|
13
20
|
}
|
|
14
21
|
export default class McpAgent {
|
|
15
22
|
private openai;
|
|
@@ -20,6 +27,8 @@ export default class McpAgent {
|
|
|
20
27
|
private engine;
|
|
21
28
|
private encoder;
|
|
22
29
|
private extraTools;
|
|
30
|
+
private maxTokens;
|
|
31
|
+
private apiConfig;
|
|
23
32
|
constructor(options?: AgentOptions);
|
|
24
33
|
/**
|
|
25
34
|
* 计算当前消息列表的总 Token 消耗
|
|
@@ -36,6 +45,11 @@ export default class McpAgent {
|
|
|
36
45
|
private loadMcpConfigs;
|
|
37
46
|
init(): Promise<void>;
|
|
38
47
|
private processChat;
|
|
48
|
+
/**
|
|
49
|
+
* 裁剪上下文消息列表
|
|
50
|
+
* 保留第一条 System 消息,并移除中间的旧消息直到低于阈值
|
|
51
|
+
*/
|
|
52
|
+
private pruneMessages;
|
|
39
53
|
/**
|
|
40
54
|
* 简易 Loading 动画辅助函数
|
|
41
55
|
*/
|
|
@@ -48,3 +62,4 @@ export default class McpAgent {
|
|
|
48
62
|
chat(input: string): Promise<string>;
|
|
49
63
|
start(): Promise<void>;
|
|
50
64
|
}
|
|
65
|
+
export {};
|
package/lib/agent.js
CHANGED
|
@@ -56,11 +56,26 @@ class McpAgent {
|
|
|
56
56
|
this.extraTools = [];
|
|
57
57
|
this.engine = new ts_context_mcp_1.PromptEngine((options === null || options === void 0 ? void 0 : options.targetDir) || process.cwd());
|
|
58
58
|
this.extraTools = (options === null || options === void 0 ? void 0 : options.tools) || []; // 接收外部传入的工具
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
this.maxTokens = (options === null || options === void 0 ? void 0 : options.maxTokens) || 100000; // 默认 100k
|
|
60
|
+
this.apiConfig = options === null || options === void 0 ? void 0 : options.apiConfig;
|
|
61
|
+
let baseSystemPrompt = `你是一个专业的 AI 代码架构师,具备深度的源码分析与工程化处理能力。
|
|
62
|
+
|
|
63
|
+
### 核心操作规范:
|
|
64
|
+
1. **全局扫描(强制首选)**:在开始任何分析任务前,你【必须】首先调用 'get_repo_map'。这是你理解项目目录结构、技术栈及模块关系的唯一权威来源。
|
|
65
|
+
2. **循序渐进的分析路径**:
|
|
66
|
+
- 优先使用 'read_skeleton' 提取接口、类和函数签名,以最低的 Token 成本建立代码逻辑视图。
|
|
67
|
+
- 仅在需要深入分析具体业务逻辑、提取精准代码块或进行代码修改建议时,才使用 'read_full_code' 或 'get_method_body'。
|
|
68
|
+
3. **真实性原则**:
|
|
69
|
+
- 所有的代码分析、行号定位和逻辑推断必须基于工具返回的真实内容,严禁基于文件名进行虚假猜测。
|
|
70
|
+
- 如果工具返回结果为空或报错,应尝试调整路径或更换工具。
|
|
71
|
+
|
|
72
|
+
### 技术能力:
|
|
73
|
+
- 精通 TypeScript/JavaScript 及其 AST 结构,能准确识别各种复杂的声明与调用关系。
|
|
74
|
+
- 能够理解代码间的依赖链路,并结合项目上下文给出合理的架构建议。
|
|
75
|
+
|
|
76
|
+
### 执行准则:
|
|
77
|
+
- **任务导向**:直接通过工具链解决问题,减少不必要的中间对话。
|
|
78
|
+
- **自主决策**:根据任务需求自主选择最合适的工具组合,无需每一步都向用户请示。`;
|
|
64
79
|
// 2. 拼接额外指令
|
|
65
80
|
if (options === null || options === void 0 ? void 0 : options.extraSystemPrompt) {
|
|
66
81
|
const extra = typeof options.extraSystemPrompt === 'string'
|
|
@@ -211,7 +226,13 @@ class McpAgent {
|
|
|
211
226
|
required: ["filePath", "methodName"],
|
|
212
227
|
},
|
|
213
228
|
},
|
|
214
|
-
_handler: async ({ filePath, methodName }) =>
|
|
229
|
+
_handler: async ({ filePath, methodName }) => {
|
|
230
|
+
// --- 新增:同样的 Token 守卫 ---
|
|
231
|
+
if (this.calculateTokens() > this.maxTokens) {
|
|
232
|
+
return `[SYSTEM WARNING]: Token 消耗已达上限,禁止获取详细方法体。请利用已获取的 Skeleton 信息进行分析。`;
|
|
233
|
+
}
|
|
234
|
+
return this.engine.getMethodImplementation(filePath, methodName);
|
|
235
|
+
},
|
|
215
236
|
},
|
|
216
237
|
{
|
|
217
238
|
type: "function",
|
|
@@ -228,6 +249,11 @@ class McpAgent {
|
|
|
228
249
|
},
|
|
229
250
|
// 核心实现:直接利用 fs 读取
|
|
230
251
|
_handler: async ({ filePath }) => {
|
|
252
|
+
// --- 新增:Token 守卫 ---
|
|
253
|
+
const currentTokens = this.calculateTokens();
|
|
254
|
+
if (currentTokens > this.maxTokens) {
|
|
255
|
+
return `[SYSTEM WARNING]: 当前上下文已达到 ${currentTokens} tokens (上限 ${this.maxTokens})。为了保证系统稳定,已拦截 read_full_code。请立即根据已知信息进行总结或停止阅读更多代码。`;
|
|
256
|
+
}
|
|
231
257
|
try {
|
|
232
258
|
if (typeof filePath !== 'string' || !filePath) {
|
|
233
259
|
return "Error: filePath 不能为空";
|
|
@@ -257,6 +283,8 @@ class McpAgent {
|
|
|
257
283
|
}
|
|
258
284
|
// --- 初始化与环境准备 (API Config & MCP Servers) ---
|
|
259
285
|
async ensureApiConfig() {
|
|
286
|
+
if (this.apiConfig)
|
|
287
|
+
return this.apiConfig;
|
|
260
288
|
if (fs_1.default.existsSync(CONFIG_FILE)) {
|
|
261
289
|
return JSON.parse(fs_1.default.readFileSync(CONFIG_FILE, "utf-8"));
|
|
262
290
|
}
|
|
@@ -329,9 +357,18 @@ class McpAgent {
|
|
|
329
357
|
var _a;
|
|
330
358
|
this.messages.push({ role: 'user', content: userInput });
|
|
331
359
|
while (true) {
|
|
360
|
+
// --- 新增:发送请求前先检查并裁剪 ---
|
|
361
|
+
this.pruneMessages();
|
|
332
362
|
// 打印当前上下文的累计 Token
|
|
333
363
|
const currentInputTokens = this.calculateTokens();
|
|
334
364
|
console.log(`\n📊 当前上下文累计: ${currentInputTokens} tokens`);
|
|
365
|
+
// 如果接近上限(如 80%),在消息队列中插入一条隐含的系统指令
|
|
366
|
+
if (currentInputTokens > this.maxTokens * 0.8 && currentInputTokens <= this.maxTokens) {
|
|
367
|
+
this.messages.push({
|
|
368
|
+
role: "system",
|
|
369
|
+
content: "注意:上下文即将耗尽。请停止读取新文件,优先处理现有信息并尽快输出结果。"
|
|
370
|
+
});
|
|
371
|
+
}
|
|
335
372
|
const stopLoading = this.showLoading("🤖 Agent 正在思考...");
|
|
336
373
|
let response;
|
|
337
374
|
try {
|
|
@@ -388,6 +425,24 @@ class McpAgent {
|
|
|
388
425
|
}
|
|
389
426
|
}
|
|
390
427
|
}
|
|
428
|
+
/**
|
|
429
|
+
* 裁剪上下文消息列表
|
|
430
|
+
* 保留第一条 System 消息,并移除中间的旧消息直到低于阈值
|
|
431
|
+
*/
|
|
432
|
+
pruneMessages() {
|
|
433
|
+
const currentTokens = this.calculateTokens();
|
|
434
|
+
if (currentTokens <= this.maxTokens)
|
|
435
|
+
return;
|
|
436
|
+
console.log(`\n⚠️ 上下文达到限制 (${currentTokens} tokens),正在自动裁剪...`);
|
|
437
|
+
// 策略:保留索引 0 (System),从索引 1 开始删除
|
|
438
|
+
// 每次删除一对 (通常是助理请求 + 工具回复,或者用户提问 + 助理回答)
|
|
439
|
+
while (this.calculateTokens() > this.maxTokens && this.messages.length > 2) {
|
|
440
|
+
// 始终保留系统提示词 (index 0) 和最后一条消息 (保持对话连贯)
|
|
441
|
+
// 删除索引为 1 的消息
|
|
442
|
+
this.messages.splice(1, 1);
|
|
443
|
+
}
|
|
444
|
+
console.log(`✅ 裁剪完成,当前上下文: ${this.calculateTokens()} tokens`);
|
|
445
|
+
}
|
|
391
446
|
/**
|
|
392
447
|
* 简易 Loading 动画辅助函数
|
|
393
448
|
*/
|
package/lib/cli-chain.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const agent_chain_1 = __importDefault(require("./agent-chain"));
|
|
8
|
+
const manager = new agent_chain_1.default();
|
|
9
|
+
manager.start();
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -17,7 +17,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
17
17
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
18
18
|
};
|
|
19
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
-
exports.default = void 0;
|
|
20
|
+
exports.default = exports.McpChainAgent = void 0;
|
|
21
21
|
__exportStar(require("./agent"), exports);
|
|
22
|
+
var agent_chain_1 = require("./agent-chain");
|
|
23
|
+
Object.defineProperty(exports, "McpChainAgent", { enumerable: true, get: function () { return __importDefault(agent_chain_1).default; } });
|
|
22
24
|
var agent_1 = require("./agent");
|
|
23
25
|
Object.defineProperty(exports, "default", { enumerable: true, get: function () { return __importDefault(agent_1).default; } });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saber2pr/ai-agent",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "AI Assistant CLI.",
|
|
5
5
|
"author": "saber2pr",
|
|
6
6
|
"license": "ISC",
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"lib"
|
|
9
9
|
],
|
|
10
10
|
"bin": {
|
|
11
|
-
"sagent": "./lib/cli.js"
|
|
11
|
+
"sagent": "./lib/cli.js",
|
|
12
|
+
"sagent-chain": "./lib/cli-chain.js"
|
|
12
13
|
},
|
|
13
14
|
"publishConfig": {
|
|
14
15
|
"access": "public",
|
|
@@ -21,9 +22,12 @@
|
|
|
21
22
|
"prepublishOnly": "tsc"
|
|
22
23
|
},
|
|
23
24
|
"dependencies": {
|
|
25
|
+
"@langchain/core": "^1.1.18",
|
|
26
|
+
"@langchain/openai": "^1.2.4",
|
|
24
27
|
"@modelcontextprotocol/sdk": "^1.25.3",
|
|
25
28
|
"@saber2pr/ts-context-mcp": "^0.0.6",
|
|
26
29
|
"js-tiktoken": "^1.0.21",
|
|
30
|
+
"langchain": "~0.3",
|
|
27
31
|
"openai": "^6.16.0"
|
|
28
32
|
},
|
|
29
33
|
"devDependencies": {
|