@atlisp/agent 0.1.1

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/agent.js ADDED
@@ -0,0 +1,215 @@
1
+ import { createProvider } from './providers/index.js';
2
+ import { createMcpClient, connectMcp } from './mcp/index.js';
3
+ import { buildSystemMessage, buildToolsDescription, TOOL_DESCRIPTIONS } from './prompt.js';
4
+ import { getAgentConfig } from './config.js';
5
+
6
+ export class Agent {
7
+ constructor(options = {}) {
8
+ this.llm = options.llm || createProvider(options.llmConfig);
9
+ this.mcp = options.mcp || createMcpClient(options.mcpConfig);
10
+ this.config = { ...getAgentConfig(), ...options.agentConfig };
11
+ this.messages = [];
12
+ this.tools = null;
13
+ this.maxSteps = this.config.maxSteps || 10;
14
+ this.verbose = this.config.verbose || false;
15
+ }
16
+
17
+ async initialize() {
18
+ console.error('[Agent] 初始化...');
19
+
20
+ await connectMcp(this.mcp);
21
+ console.error('[Agent] MCP 连接成功');
22
+
23
+ this.tools = await this.mcp.listTools();
24
+ console.error(`[Agent] 已加载 ${this.tools?.tools?.length || 0} 个工具`);
25
+
26
+ this.messages = [
27
+ { role: 'system', content: buildSystemMessage() },
28
+ ];
29
+
30
+ return this;
31
+ }
32
+
33
+ async chat(message) {
34
+ this.messages.push({ role: 'user', content: message });
35
+
36
+ let step = 0;
37
+ let finalResponse = '';
38
+ let hasToolCalls = false;
39
+
40
+ while (step < this.maxSteps) {
41
+ step++;
42
+
43
+ if (this.verbose) {
44
+ console.error(`[Agent] Step ${step}/${this.maxSteps}`);
45
+ }
46
+
47
+ const response = await this.llm.chat(this.messages, {
48
+ tools: buildToolsDescription(),
49
+ });
50
+
51
+ if (!response) {
52
+ finalResponse = '抱歉,我无法理解你的请求。';
53
+ break;
54
+ }
55
+
56
+ this.messages.push({ role: 'assistant', content: response });
57
+
58
+ const toolCalls = this.extractToolCalls(response);
59
+
60
+ if (toolCalls.length === 0) {
61
+ finalResponse = response;
62
+ break;
63
+ }
64
+
65
+ hasToolCalls = true;
66
+ for (const toolCall of toolCalls) {
67
+ const result = await this.executeTool(toolCall);
68
+ this.messages.push({
69
+ role: 'user',
70
+ content: `工具 ${toolCall.name} 执行结果:\n${this.formatToolResult(result)}`,
71
+ });
72
+ }
73
+ }
74
+
75
+ if (hasToolCalls && !finalResponse) {
76
+ const finalReply = await this.llm.chat(this.messages, { tools: [] });
77
+ finalResponse = finalReply || '任务已完成';
78
+ }
79
+
80
+ return finalResponse || '任务已完成';
81
+ }
82
+
83
+ extractToolCalls(text) {
84
+ const calls = [];
85
+
86
+ const trimmed = text.trim();
87
+ if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
88
+ try {
89
+ const parsed = JSON.parse(trimmed);
90
+ if (Array.isArray(parsed)) {
91
+ for (const call of parsed) {
92
+ if (call.name) {
93
+ let args = call.arguments || {};
94
+ if (typeof args === 'string') {
95
+ try { args = JSON.parse(args); } catch {}
96
+ }
97
+ calls.push({ name: call.name, arguments: args });
98
+ }
99
+ }
100
+ if (calls.length > 0) return calls;
101
+ }
102
+ } catch (e) {}
103
+ }
104
+
105
+ const callPattern = /call:\s*(\w+)\{(\{[^\}]*\})?\}/g;
106
+ let match;
107
+ while ((match = callPattern.exec(text)) !== null) {
108
+ const name = match[1];
109
+ let args = {};
110
+ if (match[2]) {
111
+ try { args = JSON.parse(match[2]); } catch {}
112
+ }
113
+ calls.push({ name, arguments: args });
114
+ }
115
+ if (calls.length > 0) return calls;
116
+
117
+ try {
118
+ const toolCallsMatch = text.match(/tool_calls\s*:\s*(\[[\s\S]*?\])/i);
119
+ if (toolCallsMatch) {
120
+ const parsed = JSON.parse(toolCallsMatch[1]);
121
+ for (const call of parsed) {
122
+ if (call.name || call.function?.name) {
123
+ let args = call.arguments;
124
+ if (typeof args === 'string') {
125
+ try { args = JSON.parse(args); } catch {}
126
+ }
127
+ calls.push({
128
+ name: call.name || call.function.name,
129
+ arguments: args || {},
130
+ });
131
+ }
132
+ }
133
+ return calls;
134
+ }
135
+ } catch (e) {}
136
+
137
+ const jsonMatch = text.match(/```json\s*(\[[\s\S]*?\])\s*```/);
138
+ if (jsonMatch) {
139
+ try {
140
+ const parsed = JSON.parse(jsonMatch[1]);
141
+ if (Array.isArray(parsed)) {
142
+ for (const call of parsed) {
143
+ if (call.name || call.function?.name) {
144
+ calls.push({
145
+ name: call.name || call.function.name,
146
+ arguments: call.arguments || call.function.arguments || {},
147
+ });
148
+ }
149
+ }
150
+ }
151
+ } catch (e) {}
152
+ }
153
+
154
+ const toolPattern = /```tool\s*([\s\S]*?)```/g;
155
+ while ((match = toolPattern.exec(text)) !== null) {
156
+ try {
157
+ const tool = JSON.parse(match[1]);
158
+ if (tool.name) {
159
+ calls.push(tool);
160
+ }
161
+ } catch (e) {}
162
+ }
163
+
164
+ return calls;
165
+ }
166
+
167
+ async executeTool(toolCall) {
168
+ const { name, arguments: args } = toolCall;
169
+
170
+ if (this.verbose) {
171
+ console.error(`[Agent] 执行工具: ${name}`, args);
172
+ }
173
+
174
+ try {
175
+ const result = await this.mcp.callTool(name, args);
176
+
177
+ if (this.verbose) {
178
+ console.error(`[Agent] 工具结果:`, result);
179
+ }
180
+
181
+ return result;
182
+ } catch (error) {
183
+ return { error: error.message, isError: true };
184
+ }
185
+ }
186
+
187
+ formatToolResult(result) {
188
+ if (!result) return '(无返回)';
189
+
190
+ if (result.isError) {
191
+ return `错误: ${result.content?.[0]?.text || result.error || '未知错误'}`;
192
+ }
193
+
194
+ if (result.content && result.content[0]) {
195
+ return result.content[0].text || JSON.stringify(result.content[0]);
196
+ }
197
+
198
+ return JSON.stringify(result);
199
+ }
200
+
201
+ async getHistory(limit = 10) {
202
+ const history = this.messages.slice(1);
203
+ return history.slice(-limit);
204
+ }
205
+
206
+ clearHistory() {
207
+ this.messages = [this.messages[0]];
208
+ }
209
+ }
210
+
211
+ export function createAgent(options = {}) {
212
+ return new Agent(options);
213
+ }
214
+
215
+ export default Agent;
package/config.js ADDED
@@ -0,0 +1,127 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import YAML from 'yaml';
5
+
6
+ function getConfigPath() {
7
+ return path.join(os.homedir(), '.atlisp', 'atlisp.json');
8
+ }
9
+
10
+ function getYamlPath() {
11
+ return path.join(os.homedir(), '.atlisp', 'atlisp.yaml');
12
+ }
13
+
14
+ function loadConfigFile() {
15
+ const jsonPath = getConfigPath();
16
+ const yamlPath = getYamlPath();
17
+
18
+ if (fs.existsSync(jsonPath)) {
19
+ try {
20
+ const content = fs.readFileSync(jsonPath, 'utf-8');
21
+ return JSON.parse(content);
22
+ } catch (e) {
23
+ console.error(`[Config] JSON 解析失败: ${e.message}`);
24
+ }
25
+ }
26
+
27
+ if (fs.existsSync(yamlPath)) {
28
+ try {
29
+ const content = fs.readFileSync(yamlPath, 'utf-8');
30
+ return YAML.parse(content);
31
+ } catch (e) {
32
+ console.error(`[Config] YAML 解析失败: ${e.message}`);
33
+ }
34
+ }
35
+
36
+ return {};
37
+ }
38
+
39
+ function deepMerge(target, source) {
40
+ const result = { ...target };
41
+ for (const key of Object.keys(source)) {
42
+ if (source[key] !== null && typeof source[key] === 'object' && !Array.isArray(source[key])) {
43
+ result[key] = deepMerge(target[key] || {}, source[key]);
44
+ } else if (source[key] !== undefined) {
45
+ result[key] = source[key];
46
+ }
47
+ }
48
+ return result;
49
+ }
50
+
51
+ const defaultConfig = {
52
+ agent: {
53
+ enabled: true,
54
+ historyLimit: 50,
55
+ maxSteps: 10,
56
+ verbose: false,
57
+ },
58
+ llm: {
59
+ provider: process.env.LLM_PROVIDER || 'vllm',
60
+ baseURL: process.env.VLLM_BASE_URL || '',
61
+ model: process.env.LLM_MODEL || 'deepseek-chat',
62
+ apiKey: process.env.DEEPSEEK_API_KEY || '',
63
+ temperature: parseFloat(process.env.LLM_TEMPERATURE || '0.7'),
64
+ maxTokens: parseInt(process.env.LLM_MAX_TOKENS || '2048'),
65
+ },
66
+ mcp: {
67
+ mode: 'http',
68
+ url: 'http://localhost:8110',
69
+ command: '',
70
+ args: [],
71
+ },
72
+ };
73
+
74
+ let config = null;
75
+
76
+ function loadConfig() {
77
+ const fileConfig = loadConfigFile();
78
+ let merged = deepMerge(defaultConfig, fileConfig);
79
+
80
+ merged.agent = {
81
+ enabled: process.env.AGENT_ENABLED !== 'false',
82
+ historyLimit: parseInt(process.env.AGENT_HISTORY_LIMIT || String(merged.agent.historyLimit), 10),
83
+ maxSteps: parseInt(process.env.AGENT_MAX_STEPS || String(merged.agent.maxSteps), 10),
84
+ verbose: process.env.AGENT_VERBOSE === 'true' || merged.agent.verbose,
85
+ };
86
+
87
+ merged.llm = {
88
+ provider: process.env.LLM_PROVIDER || merged.llm.provider,
89
+ baseURL: process.env.VLLM_BASE_URL || merged.llm.baseURL,
90
+ model: process.env.LLM_MODEL || merged.llm.model,
91
+ apiKey: process.env.VLLM_API_KEY || merged.llm.apiKey,
92
+ temperature: parseFloat(process.env.LLM_TEMPERATURE || String(merged.llm.temperature)),
93
+ maxTokens: parseInt(process.env.LLM_MAX_TOKENS || String(merged.llm.maxTokens)),
94
+ };
95
+
96
+ merged.mcp = {
97
+ mode: process.env.MCP_MODE || merged.mcp.mode,
98
+ url: process.env.MCP_URL || merged.mcp.url,
99
+ command: process.env.MCP_COMMAND || merged.mcp.command,
100
+ args: process.env.MCP_ARGS ? process.env.MCP_ARGS.split(' ') : merged.mcp.args,
101
+ };
102
+
103
+ return merged;
104
+ }
105
+
106
+ function getConfig() {
107
+ if (!config) {
108
+ config = loadConfig();
109
+ }
110
+ return config;
111
+ }
112
+
113
+ export function getAgentConfig() {
114
+ return getConfig().agent;
115
+ }
116
+
117
+ export function getLlmConfig() {
118
+ return getConfig().llm;
119
+ }
120
+
121
+ export function getMcpConfig() {
122
+ return getConfig().mcp;
123
+ }
124
+
125
+ export { getConfig };
126
+
127
+ export default getConfig;
package/index.js ADDED
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { createAgent } from './agent.js';
5
+ import { getLlmConfig, getMcpConfig, getAgentConfig } from './config.js';
6
+
7
+ const program = new Command();
8
+
9
+ program
10
+ .name('atlisp-agent')
11
+ .description('AI Agent for @lisp - Connects to MCP Server for CAD operations')
12
+ .version('1.0.0');
13
+
14
+ program
15
+ .option('-m, --mode <mode>', 'MCP mode: http or stdio', 'http')
16
+ .option('-u, --mcp-url <url>', 'MCP HTTP URL', getMcpConfig().url)
17
+ .option('-c, --mcp-command <cmd>', 'MCP stdio command', getMcpConfig().command)
18
+ .option('--mcp-args <args>', 'MCP stdio args', '')
19
+ .option('-v, --verbose', 'Verbose output', false)
20
+ .option('--no-color', 'Disable color output', false);
21
+
22
+ program
23
+ .command('chat')
24
+ .description('Start interactive chat session')
25
+ .argument('[message]', 'Initial message to send')
26
+ .action(async (message) => {
27
+ await runChat({ message, interactive: !message });
28
+ });
29
+
30
+ program
31
+ .command('exec')
32
+ .description('Execute a single command')
33
+ .argument('<message>', 'Message to send to agent')
34
+ .action(async (message) => {
35
+ const result = await runSingle(message);
36
+ console.log(result);
37
+ });
38
+
39
+ program
40
+ .command('tools')
41
+ .description('List available MCP tools')
42
+ .action(async () => {
43
+ await listTools();
44
+ });
45
+
46
+ program.parse(process.argv);
47
+
48
+ async function getAgent() {
49
+ const config = program.opts();
50
+ const mcpConfig = {
51
+ mode: config.mode,
52
+ url: config.mcpUrl,
53
+ command: config.mcpCommand,
54
+ args: config.mcpArgs ? config.mcpArgs.split(' ') : [],
55
+ };
56
+
57
+ return createAgent({
58
+ mcpConfig,
59
+ agentConfig: { verbose: config.verbose },
60
+ });
61
+ }
62
+
63
+ async function runChat(options) {
64
+ const agent = await getAgent();
65
+ await agent.initialize();
66
+
67
+ console.error('atlisp-agent 已启动,输入你的问题(或输入 exit 退出)\n');
68
+
69
+ if (options.message) {
70
+ const response = await agent.chat(options.message);
71
+ console.log(response);
72
+ }
73
+
74
+ const readline = await import('readline');
75
+ const rl = readline.createInterface({
76
+ input: process.stdin,
77
+ output: process.stdout,
78
+ prompt: '> ',
79
+ });
80
+
81
+ rl.prompt();
82
+
83
+ rl.on('line', async (line) => {
84
+ const input = line.trim();
85
+ if (input === 'exit' || input === 'quit') {
86
+ rl.close();
87
+ return;
88
+ }
89
+
90
+ if (!input) {
91
+ rl.prompt();
92
+ return;
93
+ }
94
+
95
+ try {
96
+ const response = await agent.chat(input);
97
+ console.log('\n' + response + '\n');
98
+ } catch (err) {
99
+ console.error('\n错误:', err.message);
100
+ }
101
+
102
+ rl.prompt();
103
+ });
104
+
105
+ rl.on('close', () => {
106
+ console.error('\n再见!');
107
+ process.exit(0);
108
+ });
109
+ }
110
+
111
+ async function runSingle(message) {
112
+ const agent = await getAgent();
113
+ await agent.initialize();
114
+ return agent.chat(message);
115
+ }
116
+
117
+ async function listTools() {
118
+ const agent = await getAgent();
119
+ await agent.initialize();
120
+
121
+ const tools = agent.tools?.tools || [];
122
+ console.log(`可用工具 (${tools.length}):\n`);
123
+
124
+ for (const tool of tools) {
125
+ console.log(` ${tool.name}`);
126
+ console.log(` ${tool.description}`);
127
+ console.log();
128
+ }
129
+ }
130
+
131
+ if (process.argv.length === 2) {
132
+ program.help();
133
+ }
@@ -0,0 +1,80 @@
1
+ import { getMcpConfig } from '../config.js';
2
+
3
+ export class HttpMcpClient {
4
+ constructor(config = {}) {
5
+ this.config = { ...getMcpConfig(), ...config };
6
+ this.baseURL = this.config.url.replace(/\/$/, '') + '/mcp/message';
7
+ this.apiKey = this.config.apiKey || '';
8
+ this.requestId = 0;
9
+ }
10
+
11
+ async request(method, params = {}) {
12
+ const id = ++this.requestId;
13
+ const body = {
14
+ jsonrpc: '2.0',
15
+ id,
16
+ method,
17
+ params,
18
+ };
19
+
20
+ const response = await fetch(this.baseURL, {
21
+ method: 'POST',
22
+ headers: {
23
+ 'Content-Type': 'application/json',
24
+ ...(this.apiKey && { 'Authorization': `Bearer ${this.apiKey}` }),
25
+ },
26
+ body: JSON.stringify(body),
27
+ });
28
+
29
+ if (!response.ok) {
30
+ throw new Error(`MCP HTTP error: ${response.status} ${response.statusText}`);
31
+ }
32
+
33
+ const data = await response.json();
34
+
35
+ if (data.error) {
36
+ throw new Error(`MCP error: ${data.error.message || JSON.stringify(data.error)}`);
37
+ }
38
+
39
+ return data.result;
40
+ }
41
+
42
+ async listTools() {
43
+ return this.request('tools/list');
44
+ }
45
+
46
+ async callTool(name, args = {}) {
47
+ return this.request('tools/call', { name, arguments: args });
48
+ }
49
+
50
+ async listResources() {
51
+ return this.request('resources/list');
52
+ }
53
+
54
+ async readResource(uri) {
55
+ return this.request('resources/read', { uri });
56
+ }
57
+
58
+ async listPrompts() {
59
+ return this.request('prompts/list');
60
+ }
61
+
62
+ async getPrompt(name, args = {}) {
63
+ return this.request('prompts/get', { name, arguments: args });
64
+ }
65
+
66
+ async initialize() {
67
+ const result = await this.request('initialize', {
68
+ protocolVersion: '2024-11-05',
69
+ capabilities: {},
70
+ clientInfo: { name: 'atlisp-agent', version: '1.0.0' },
71
+ });
72
+ return result;
73
+ }
74
+
75
+ getTools() {
76
+ return this.listTools();
77
+ }
78
+ }
79
+
80
+ export default HttpMcpClient;
package/mcp/index.js ADDED
@@ -0,0 +1,26 @@
1
+ import { HttpMcpClient } from './http-client.js';
2
+ import { StdioMcpClient } from './stdio-client.js';
3
+ import { getMcpConfig } from '../config.js';
4
+
5
+ export function createMcpClient(config = {}) {
6
+ const mcpConfig = { ...getMcpConfig(), ...config };
7
+ const mode = mcpConfig.mode || 'http';
8
+
9
+ if (mode === 'stdio') {
10
+ return new StdioMcpClient(config);
11
+ }
12
+
13
+ return new HttpMcpClient(config);
14
+ }
15
+
16
+ export async function connectMcp(client) {
17
+ if (client.connect) {
18
+ await client.connect();
19
+ }
20
+ if (client.initialize) {
21
+ await client.initialize();
22
+ }
23
+ return client;
24
+ }
25
+
26
+ export { HttpMcpClient, StdioMcpClient };
@@ -0,0 +1,143 @@
1
+ import { spawn } from 'child_process';
2
+ import { getMcpConfig } from '../config.js';
3
+
4
+ export class StdioMcpClient {
5
+ constructor(config = {}) {
6
+ this.config = { ...getMcpConfig(), ...config };
7
+ this.command = this.config.command;
8
+ this.args = this.config.args || [];
9
+ this.proc = null;
10
+ this.requestId = 0;
11
+ this.pendingRequests = new Map();
12
+ this.buffer = '';
13
+ this.initialized = false;
14
+ }
15
+
16
+ async connect() {
17
+ return new Promise((resolve, reject) => {
18
+ try {
19
+ this.proc = spawn(this.command, this.args, {
20
+ stdio: ['pipe', 'pipe', 'pipe'],
21
+ });
22
+
23
+ this.proc.stdout.on('data', (data) => {
24
+ this.handleStdout(data.toString());
25
+ });
26
+
27
+ this.proc.stderr.on('data', (data) => {
28
+ console.error('[MCP Stderr]:', data.toString());
29
+ });
30
+
31
+ this.proc.on('exit', (code) => {
32
+ console.error(`[MCP Stdio] Process exited with code ${code}`);
33
+ this.initialized = false;
34
+ });
35
+
36
+ this.proc.on('error', (err) => {
37
+ console.error('[MCP Stdio] Process error:', err.message);
38
+ reject(err);
39
+ });
40
+
41
+ setTimeout(() => {
42
+ resolve(true);
43
+ }, 1000);
44
+ } catch (err) {
45
+ reject(err);
46
+ }
47
+ });
48
+ }
49
+
50
+ handleStdout(data) {
51
+ this.buffer += data;
52
+ const lines = this.buffer.split('\n');
53
+ this.buffer = lines.pop() || '';
54
+
55
+ for (const line of lines) {
56
+ if (!line.trim()) continue;
57
+ try {
58
+ const msg = JSON.parse(line);
59
+ const resolve = this.pendingRequests.get(msg.id);
60
+ if (resolve) {
61
+ if (msg.error) {
62
+ resolve(Promise.reject(new Error(msg.error.message || JSON.stringify(msg.error))));
63
+ } else {
64
+ resolve(msg.result);
65
+ }
66
+ this.pendingRequests.delete(msg.id);
67
+ }
68
+ } catch (e) {
69
+ console.warn('[MCP Stdio] Parse error:', e.message);
70
+ }
71
+ }
72
+ }
73
+
74
+ async request(method, params = {}) {
75
+ return new Promise((resolve, reject) => {
76
+ const id = ++this.requestId;
77
+ const body = {
78
+ jsonrpc: '2.0',
79
+ id,
80
+ method,
81
+ params,
82
+ };
83
+
84
+ this.pendingRequests.set(id, resolve);
85
+
86
+ this.proc.stdin.write(JSON.stringify(body) + '\n');
87
+
88
+ setTimeout(() => {
89
+ if (this.pendingRequests.has(id)) {
90
+ this.pendingRequests.delete(id);
91
+ reject(new Error(`MCP request timeout: ${method}`));
92
+ }
93
+ }, 30000);
94
+ });
95
+ }
96
+
97
+ async listTools() {
98
+ return this.request('tools/list');
99
+ }
100
+
101
+ async callTool(name, args = {}) {
102
+ return this.request('tools/call', { name, arguments: args });
103
+ }
104
+
105
+ async listResources() {
106
+ return this.request('resources/list');
107
+ }
108
+
109
+ async readResource(uri) {
110
+ return this.request('resources/read', { uri });
111
+ }
112
+
113
+ async listPrompts() {
114
+ return this.request('prompts/list');
115
+ }
116
+
117
+ async getPrompt(name, args = {}) {
118
+ return this.request('prompts/get', { name, arguments: args });
119
+ }
120
+
121
+ async initialize() {
122
+ const result = await this.request('initialize', {
123
+ protocolVersion: '2024-11-05',
124
+ capabilities: {},
125
+ clientInfo: { name: 'atlisp-agent', version: '1.0.0' },
126
+ });
127
+ this.initialized = true;
128
+ return result;
129
+ }
130
+
131
+ async getTools() {
132
+ return this.listTools();
133
+ }
134
+
135
+ disconnect() {
136
+ if (this.proc) {
137
+ this.proc.kill();
138
+ this.proc = null;
139
+ }
140
+ }
141
+ }
142
+
143
+ export default StdioMcpClient;
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@atlisp/agent",
3
+ "version": "0.1.1",
4
+ "description": "AI Agent for @lisp - Connects to MCP Server for CAD operations",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "atlisp-agent": "./index.js",
9
+ "atlisp-agent-server": "./server.js"
10
+ },
11
+ "scripts": {
12
+ "start": "node index.js",
13
+ "server": "node server.js",
14
+ "test": "echo \"No tests yet\""
15
+ },
16
+ "keywords": [
17
+ "atlisp",
18
+ "agent",
19
+ "ai",
20
+ "cad",
21
+ "autolisp",
22
+ "mcp"
23
+ ],
24
+ "author": "vitalgg",
25
+ "license": "ISC",
26
+ "dependencies": {
27
+ "commander": "^11.1.0",
28
+ "express": "^5.2.1",
29
+ "yaml": "^2.6.1"
30
+ }
31
+ }
package/prompt.js ADDED
@@ -0,0 +1,165 @@
1
+ export const SYSTEM_PROMPT = `你是一个专业的 @lisp AI 助手,专门帮助用户操作 CAD(AutoCAD/ZWCAD/GStarCAD/BricsCAD)和 AutoLISP 编程。
2
+
3
+ ## 你的能力
4
+
5
+ 1. **CAD 操作**: 通过 MCP 工具执行 CAD 命令、操作图形、管理图层等
6
+ 2. **AutoLISP 编程**: 编写、解释、调试 AutoLISP 代码
7
+ 3. **包管理**: 搜索、安装、列出 @lisp 包
8
+ 4. **函数查询**: 查询 @lisp 函数库中的函数用法
9
+
10
+ ## 重要规则
11
+
12
+ - 每次回复应简洁明了
13
+ - 执行工具后,将结果格式化呈现给用户
14
+ - 如果工具执行失败,说明错误原因并尝试修复
15
+ - 不要假设 CAD 状态,始终先检查连接状态
16
+ - **当需要执行工具时,必须使用以下 JSON 格式返回**:
17
+ [{"name": "工具名称", "arguments": {"参数": "值"}}]
18
+ - **不要使用其他格式,只使用这个 JSON 数组格式**
19
+
20
+ ## 可用工具
21
+
22
+ `;
23
+
24
+ export const TOOL_DESCRIPTIONS = [
25
+ {
26
+ name: 'connect_cad',
27
+ description: '连接到 CAD (AutoCAD/ZWCAD/GStarCAD/BricsCAD)',
28
+ inputSchema: { type: 'object', properties: {} },
29
+ },
30
+ {
31
+ name: 'eval_lisp',
32
+ description: '在 CAD 中执行 AutoLISP 代码(不返回结果)',
33
+ inputSchema: {
34
+ type: 'object',
35
+ properties: { code: { type: 'string', description: '要执行的 LISP 代码' } },
36
+ required: ['code'],
37
+ },
38
+ },
39
+ {
40
+ name: 'eval_lisp_with_result',
41
+ description: '在 CAD 中执行 AutoLISP 代码并返回结果',
42
+ inputSchema: {
43
+ type: 'object',
44
+ properties: {
45
+ code: { type: 'string', description: '要执行的 LISP 代码' },
46
+ encoding: { type: 'string', description: '结果编码,如 utf-8, gbk, gb2312, gb18030(可选)' },
47
+ },
48
+ required: ['code'],
49
+ },
50
+ },
51
+ {
52
+ name: 'get_cad_info',
53
+ description: '获取当前 CAD 信息(平台、版本、连接状态)',
54
+ inputSchema: { type: 'object', properties: {} },
55
+ },
56
+ {
57
+ name: 'list_packages',
58
+ description: '列出已安装的 @lisp 包',
59
+ inputSchema: { type: 'object', properties: {} },
60
+ },
61
+ {
62
+ name: 'search_packages',
63
+ description: '搜索 @lisp 包',
64
+ inputSchema: {
65
+ type: 'object',
66
+ properties: { query: { type: 'string', description: '搜索关键词' } },
67
+ required: ['query'],
68
+ },
69
+ },
70
+ {
71
+ name: 'install_package',
72
+ description: '安装 @lisp 包到 CAD',
73
+ inputSchema: {
74
+ type: 'object',
75
+ properties: { packageName: { type: 'string', description: '包名称' } },
76
+ required: ['packageName'],
77
+ },
78
+ },
79
+ {
80
+ name: 'get_platform_info',
81
+ description: '获取 CAD 平台信息',
82
+ inputSchema: { type: 'object', properties: {} },
83
+ },
84
+ {
85
+ name: 'get_file_extensions',
86
+ description: '获取指定平台的文件扩展名',
87
+ inputSchema: {
88
+ type: 'object',
89
+ properties: { platform: { type: 'string', enum: ['AutoCAD', 'ZWCAD', 'GStarCAD', 'BricsCAD'] } },
90
+ required: ['platform'],
91
+ },
92
+ },
93
+ {
94
+ name: 'get_install_code',
95
+ description: '获取 @lisp 安装代码',
96
+ inputSchema: { type: 'object', properties: {} },
97
+ },
98
+ {
99
+ name: 'at_command',
100
+ description: '执行 @lisp 命令(如 @H, @list, @search 等)',
101
+ inputSchema: {
102
+ type: 'object',
103
+ properties: { command: { type: 'string', description: '@lisp 命令' } },
104
+ required: ['command'],
105
+ },
106
+ },
107
+ {
108
+ name: 'new_document',
109
+ description: '在 CAD 中新建空白文档',
110
+ inputSchema: { type: 'object', properties: {} },
111
+ },
112
+ {
113
+ name: 'bring_to_front',
114
+ description: '将 CAD 窗口切换到前台(从后台运行状态唤醒)',
115
+ inputSchema: { type: 'object', properties: {} },
116
+ },
117
+ {
118
+ name: 'install_atlisp',
119
+ description: '在 CAD 中安装 @lisp',
120
+ inputSchema: { type: 'object', properties: {} },
121
+ },
122
+ {
123
+ name: 'get_function_usage',
124
+ description: '获取 @lisp 函数用法(从本地 JSON 库)',
125
+ inputSchema: {
126
+ type: 'object',
127
+ properties: {
128
+ name: { type: 'string', description: '函数名,如 string:length' },
129
+ package: { type: 'string', description: '包名,如 string(可选)' },
130
+ },
131
+ required: ['name'],
132
+ },
133
+ },
134
+ {
135
+ name: 'list_functions',
136
+ description: '列出 @lisp 函数库中的所有函数',
137
+ inputSchema: {
138
+ type: 'object',
139
+ properties: { package: { type: 'string', description: '包名,如 string(可选)' } },
140
+ },
141
+ },
142
+ ];
143
+
144
+ export function buildToolsDescription() {
145
+ return TOOL_DESCRIPTIONS.map((tool) => ({
146
+ type: 'function',
147
+ function: {
148
+ name: tool.name,
149
+ description: tool.description,
150
+ parameters: tool.inputSchema,
151
+ },
152
+ }));
153
+ }
154
+
155
+ export function buildSystemMessage() {
156
+ const toolsDesc = buildToolsDescription()
157
+ .map((t) => `- ${t.function.name}: ${t.function.description}`)
158
+ .join('\n');
159
+
160
+ return SYSTEM_PROMPT + toolsDesc;
161
+ }
162
+
163
+ export function buildToolCallRequest(tools) {
164
+ return buildToolsDescription();
165
+ }
@@ -0,0 +1,107 @@
1
+ import { getLlmConfig } from '../config.js';
2
+
3
+ export class DeepseekProvider {
4
+ constructor(config = {}) {
5
+ this.config = { ...getLlmConfig(), ...config };
6
+ this.baseURL = this.config.baseURL || 'https://api.deepseek.com/v1';
7
+ this.model = this.config.model || 'deepseek-chat';
8
+ this.apiKey = this.config.apiKey;
9
+ this.temperature = this.config.temperature;
10
+ this.maxTokens = this.config.maxTokens;
11
+ }
12
+
13
+ async chat(messages, options = {}) {
14
+ const url = `${this.baseURL}/chat/completions`;
15
+ const body = {
16
+ model: this.model,
17
+ messages: messages,
18
+ temperature: options.temperature ?? this.temperature,
19
+ max_tokens: options.maxTokens ?? this.maxTokens,
20
+ ...(options.stop && { stop: options.stop }),
21
+ };
22
+
23
+ try {
24
+ const response = await fetch(url, {
25
+ method: 'POST',
26
+ headers: {
27
+ 'Content-Type': 'application/json',
28
+ 'Authorization': `Bearer ${this.apiKey}`,
29
+ },
30
+ body: JSON.stringify(body),
31
+ });
32
+
33
+ if (!response.ok) {
34
+ const errorText = await response.text();
35
+ throw new Error(`DeepSeek API error: ${response.status} ${errorText}`);
36
+ }
37
+
38
+ const data = await response.json();
39
+
40
+ if (data.choices && data.choices.length > 0) {
41
+ return data.choices[0].message.content;
42
+ }
43
+
44
+ throw new Error('No response content from DeepSeek');
45
+ } catch (error) {
46
+ if (error.message.includes('fetch')) {
47
+ throw new Error(`无法连接到 DeepSeek: ${error.message}`);
48
+ }
49
+ throw error;
50
+ }
51
+ }
52
+
53
+ async chatStream(messages, onChunk, options = {}) {
54
+ const url = `${this.baseURL}/chat/completions`;
55
+ const body = {
56
+ model: this.model,
57
+ messages: messages,
58
+ temperature: options.temperature ?? this.temperature,
59
+ max_tokens: options.maxTokens ?? this.maxTokens,
60
+ stream: true,
61
+ };
62
+
63
+ const response = await fetch(url, {
64
+ method: 'POST',
65
+ headers: {
66
+ 'Content-Type': 'application/json',
67
+ 'Authorization': `Bearer ${this.apiKey}`,
68
+ },
69
+ body: JSON.stringify(body),
70
+ });
71
+
72
+ if (!response.ok) {
73
+ throw new Error(`DeepSeek API error: ${response.status}`);
74
+ }
75
+
76
+ const reader = response.body.getReader();
77
+ const decoder = new TextDecoder();
78
+ let buffer = '';
79
+
80
+ while (true) {
81
+ const { done, value } = await reader.read();
82
+ if (done) break;
83
+
84
+ buffer += decoder.decode(value, { stream: true });
85
+ const lines = buffer.split('\n');
86
+ buffer = lines.pop() || '';
87
+
88
+ for (const line of lines) {
89
+ if (line.startsWith('data: ')) {
90
+ const data = line.slice(6);
91
+ if (data === '[DONE]') return;
92
+ try {
93
+ const obj = JSON.parse(data);
94
+ const content = obj.choices?.[0]?.delta?.content;
95
+ if (content) onChunk(content);
96
+ } catch (e) {}
97
+ }
98
+ }
99
+ }
100
+ }
101
+
102
+ getName() {
103
+ return 'deepseek';
104
+ }
105
+ }
106
+
107
+ export default DeepseekProvider;
@@ -0,0 +1,25 @@
1
+ import { VllmProvider } from './vllm.js';
2
+ import { DeepseekProvider } from './deepseek.js';
3
+ import { getLlmConfig } from '../config.js';
4
+
5
+ const PROVIDERS = {
6
+ vllm: VllmProvider,
7
+ deepseek: DeepseekProvider,
8
+ };
9
+
10
+ export function createProvider(config = {}) {
11
+ const llmConfig = { ...getLlmConfig(), ...config };
12
+ const ProviderClass = PROVIDERS[llmConfig.provider];
13
+
14
+ if (!ProviderClass) {
15
+ throw new Error(`Unknown LLM provider: ${llmConfig.provider}. Available: ${Object.keys(PROVIDERS).join(', ')}`);
16
+ }
17
+
18
+ return new ProviderClass(config);
19
+ }
20
+
21
+ export function listProviders() {
22
+ return Object.keys(PROVIDERS);
23
+ }
24
+
25
+ export { VllmProvider };
@@ -0,0 +1,107 @@
1
+ import { getLlmConfig } from '../config.js';
2
+
3
+ export class VllmProvider {
4
+ constructor(config = {}) {
5
+ this.config = { ...getLlmConfig(), ...config };
6
+ this.baseURL = this.config.baseURL;
7
+ this.model = this.config.model;
8
+ this.apiKey = this.config.apiKey;
9
+ this.temperature = this.config.temperature;
10
+ this.maxTokens = this.config.maxTokens;
11
+ }
12
+
13
+ async chat(messages, options = {}) {
14
+ const url = `${this.baseURL}/chat/completions`;
15
+ const body = {
16
+ model: this.model,
17
+ messages: messages,
18
+ temperature: options.temperature ?? this.temperature,
19
+ max_tokens: options.maxTokens ?? this.maxTokens,
20
+ ...(options.stop && { stop: options.stop }),
21
+ };
22
+
23
+ try {
24
+ const response = await fetch(url, {
25
+ method: 'POST',
26
+ headers: {
27
+ 'Content-Type': 'application/json',
28
+ 'Authorization': `Bearer ${this.apiKey}`,
29
+ },
30
+ body: JSON.stringify(body),
31
+ });
32
+
33
+ if (!response.ok) {
34
+ const errorText = await response.text();
35
+ throw new Error(`vLLM API error: ${response.status} ${errorText}`);
36
+ }
37
+
38
+ const data = await response.json();
39
+
40
+ if (data.choices && data.choices.length > 0) {
41
+ return data.choices[0].message.content;
42
+ }
43
+
44
+ throw new Error('No response content from vLLM');
45
+ } catch (error) {
46
+ if (error.message.includes('fetch')) {
47
+ throw new Error(`无法连接到 vLLM: ${error.message}`);
48
+ }
49
+ throw error;
50
+ }
51
+ }
52
+
53
+ async chatStream(messages, onChunk, options = {}) {
54
+ const url = `${this.baseURL}/chat/completions`;
55
+ const body = {
56
+ model: this.model,
57
+ messages: messages,
58
+ temperature: options.temperature ?? this.temperature,
59
+ max_tokens: options.maxTokens ?? this.maxTokens,
60
+ stream: true,
61
+ };
62
+
63
+ const response = await fetch(url, {
64
+ method: 'POST',
65
+ headers: {
66
+ 'Content-Type': 'application/json',
67
+ 'Authorization': `Bearer ${this.apiKey}`,
68
+ },
69
+ body: JSON.stringify(body),
70
+ });
71
+
72
+ if (!response.ok) {
73
+ throw new Error(`vLLM API error: ${response.status}`);
74
+ }
75
+
76
+ const reader = response.body.getReader();
77
+ const decoder = new TextDecoder();
78
+ let buffer = '';
79
+
80
+ while (true) {
81
+ const { done, value } = await reader.read();
82
+ if (done) break;
83
+
84
+ buffer += decoder.decode(value, { stream: true });
85
+ const lines = buffer.split('\n');
86
+ buffer = lines.pop() || '';
87
+
88
+ for (const line of lines) {
89
+ if (line.startsWith('data: ')) {
90
+ const data = line.slice(6);
91
+ if (data === '[DONE]') return;
92
+ try {
93
+ const obj = JSON.parse(data);
94
+ const content = obj.choices?.[0]?.delta?.content;
95
+ if (content) onChunk(content);
96
+ } catch (e) {}
97
+ }
98
+ }
99
+ }
100
+ }
101
+
102
+ getName() {
103
+ return 'vllm';
104
+ }
105
+ }
106
+
107
+ export default VllmProvider;
package/server.js ADDED
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env node
2
+
3
+ import express from 'express';
4
+ import { createAgent } from './agent.js';
5
+ import { getLlmConfig, getMcpConfig, getAgentConfig } from './config.js';
6
+
7
+ const app = express();
8
+
9
+ const PORT = process.env.AGENT_PORT || '8120';
10
+ const HOST = process.env.AGENT_HOST || '0.0.0.0';
11
+
12
+ app.use(express.json({ limit: '10mb' }));
13
+
14
+ let agent = null;
15
+
16
+ async function getAgent() {
17
+ if (!agent) {
18
+ const llmConfig = getLlmConfig();
19
+ const mcpConfig = getMcpConfig();
20
+ const agentConfig = getAgentConfig();
21
+
22
+ agent = createAgent({
23
+ llmConfig,
24
+ mcpConfig: { ...mcpConfig, url: process.env.MCP_URL || mcpConfig.url },
25
+ agentConfig: { ...agentConfig, verbose: process.env.AGENT_VERBOSE === 'true' },
26
+ });
27
+
28
+ await agent.initialize();
29
+ console.error('[Agent Server] Agent 初始化完成');
30
+ }
31
+ return agent;
32
+ }
33
+
34
+ app.get('/health', async (req, res) => {
35
+ try {
36
+ const a = await getAgent();
37
+ res.json({
38
+ status: 'ok',
39
+ tools: a.tools?.tools?.length || 0,
40
+ provider: a.llm.getName(),
41
+ });
42
+ } catch (e) {
43
+ res.status(500).json({ status: 'error', error: e.message });
44
+ }
45
+ });
46
+
47
+ app.get('/tools', async (req, res) => {
48
+ try {
49
+ const a = await getAgent();
50
+ const tools = a.tools?.tools || [];
51
+ res.json({ tools });
52
+ } catch (e) {
53
+ res.status(500).json({ error: e.message });
54
+ }
55
+ });
56
+
57
+ app.post('/chat', async (req, res) => {
58
+ try {
59
+ const a = await getAgent();
60
+ const { message, context } = req.body;
61
+
62
+ if (!message) {
63
+ return res.status(400).json({ error: '缺少 message 参数' });
64
+ }
65
+
66
+ if (context) {
67
+ for (const msg of context) {
68
+ if (msg.role && msg.content) {
69
+ a.messages.push({ role: msg.role, content: msg.content });
70
+ }
71
+ }
72
+ }
73
+
74
+ const response = await a.chat(message);
75
+ res.json({ response });
76
+ } catch (e) {
77
+ res.status(500).json({ error: e.message });
78
+ }
79
+ });
80
+
81
+ app.post('/exec', async (req, res) => {
82
+ try {
83
+ const a = await getAgent();
84
+ const { message } = req.body;
85
+
86
+ if (!message) {
87
+ return res.status(400).json({ error: '缺少 message 参数' });
88
+ }
89
+
90
+ const response = await a.chat(message);
91
+ res.json({ result: response });
92
+ } catch (e) {
93
+ res.status(500).json({ error: e.message });
94
+ }
95
+ });
96
+
97
+ app.get('/history', async (req, res) => {
98
+ try {
99
+ const a = await getAgent();
100
+ const limit = parseInt(req.query.limit || '10', 10);
101
+ const history = await a.getHistory(limit);
102
+ res.json({ history });
103
+ } catch (e) {
104
+ res.status(500).json({ error: e.message });
105
+ }
106
+ });
107
+
108
+ app.delete('/history', async (req, res) => {
109
+ try {
110
+ const a = await getAgent();
111
+ a.clearHistory();
112
+ res.json({ status: 'cleared' });
113
+ } catch (e) {
114
+ res.status(500).json({ error: e.message });
115
+ }
116
+ });
117
+
118
+ async function startServer() {
119
+ await getAgent();
120
+
121
+ app.listen(PORT, HOST, () => {
122
+ console.error(`atlisp-agent HTTP Server 启动 - http://${HOST}:${PORT}`);
123
+ console.error(`端点: POST /chat, POST /exec, GET /tools, GET /health`);
124
+ });
125
+ }
126
+
127
+ startServer().catch((e) => {
128
+ console.error('启动失败:', e);
129
+ process.exit(1);
130
+ });