Skip to content

Commit c2a31b5

Browse files
committed
feat: implement agent cli
1 parent e6b6afe commit c2a31b5

File tree

4 files changed

+172
-22
lines changed

4 files changed

+172
-22
lines changed

package-lock.json

Lines changed: 19 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/agent-api/README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,23 @@ You can use the `api.http` file to test the API using the [REST Client](https://
1515
### `npm run build`
1616

1717
To build the API for production to the `dist` folder.
18+
19+
### `npm run cli`
20+
21+
Run the burger agent CLI. Usage examples:
22+
23+
```bash
24+
# Ask a simple question
25+
npm run cli "show me the burger menu"
26+
27+
# Ask with a specific user ID
28+
npm run cli "place an order" -- --userId user123
29+
30+
# Start a new session
31+
npm run cli "hello" -- --new
32+
33+
# Combine options
34+
npm run cli "cancel my order" -- --userId user123 --new
35+
```
36+
37+
The CLI maintains conversation history in `~/.burger-agent-cli/burger-agent-cli.json` for context across sessions.

packages/agent-api/agent-cli.ts

Lines changed: 129 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
#!/usr/bin/env node
2+
13
import { AzureChatOpenAI } from '@langchain/openai';
24
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
35
import { ChatPromptTemplate } from '@langchain/core/prompts';
@@ -7,30 +9,117 @@ import { loadMcpTools } from '@langchain/mcp-adapters';
79
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
810
import { getAzureOpenAiTokenProvider } from './src/auth.js';
911
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
12+
import { BaseMessage, HumanMessage, AIMessage } from '@langchain/core/messages';
1013
import path from 'node:path';
14+
import fs from 'node:fs/promises';
15+
import os from 'node:os';
1116
import dotenv from 'dotenv';
1217

13-
dotenv.config({ path: path.join(process.cwd(), '../../.env') });
18+
dotenv.config({ path: path.join(process.cwd(), '../../.env'), quiet: true });
1419

1520
const agentSystemPrompt = `
16-
# Role
21+
## Role
1722
You an expert assistant that helps users with managing burger orders. Use the provided tools to get the information you need and perform actions on behalf of the user.
1823
Only answer to requests that are related to burger orders and the menu. If the user asks for something else, politely inform them that you can only assist with burger orders.
1924
20-
# Task
25+
## Task
2126
Help the user with their request, ask any clarifying questions if needed.
2227
23-
# Instructions
28+
## Instructions
2429
- Always use the tools provided to get the information requested or perform any actions
2530
- If you get any errors when trying to use a tool that does not seem related to missing parameters, try again
2631
- If you cannot get the information needed to answer the user's question or perform the specified action, inform the user that you are unable to do so. Never make up information.
2732
- The get_burger tool can help you get informations about the burgers
2833
- Creating or cancelling an order requires a \`userId\`: if not provided, ask the user to provide it or to run the CLI with the \`--userId\` option.
34+
35+
## Output
36+
Your response will be printed to a terminal. Do not use markdown formatting or any other special formatting. Just provide the plain text response.
2937
`;
3038

31-
const question = 'show me the burger menu';
39+
interface CliArgs {
40+
question: string;
41+
userId?: string;
42+
isNew: boolean;
43+
}
44+
45+
interface SessionData {
46+
history: Array<{ type: 'human' | 'ai'; content: string }>;
47+
userId?: string;
48+
}
49+
50+
function parseArgs(): CliArgs {
51+
const args = process.argv.slice(2);
52+
53+
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
54+
console.log('Usage: agent-cli <question> [--userId <userId>] [--new]');
55+
console.log(' question: Your question about burger orders');
56+
console.log(' --userId: Optional user ID (needed for some tasks)');
57+
console.log(' --new: Start a new session');
58+
process.exit(0);
59+
}
60+
61+
const questionParts: string[] = [];
62+
let userId: string | undefined;
63+
let isNew = false;
64+
65+
for (let i = 0; i < args.length; i++) {
66+
const arg = args[i];
67+
68+
if (arg === '--userId') {
69+
userId = args[i + 1];
70+
i++;
71+
} else if (arg === '--new') {
72+
isNew = true;
73+
} else {
74+
questionParts.push(arg);
75+
}
76+
}
77+
78+
const question = questionParts.join(' ');
79+
80+
if (!question) {
81+
console.error('Error: Question is required');
82+
process.exit(1);
83+
}
84+
85+
return { question, userId, isNew };
86+
}
87+
88+
async function getSessionPath(): Promise<string> {
89+
const userDataDir = path.join(os.homedir(), '.burger-agent-cli');
90+
await fs.mkdir(userDataDir, { recursive: true });
91+
return path.join(userDataDir, 'burger-agent-cli.json');
92+
}
93+
94+
async function loadSession(): Promise<SessionData> {
95+
try {
96+
const sessionPath = await getSessionPath();
97+
const content = await fs.readFile(sessionPath, 'utf-8');
98+
return JSON.parse(content);
99+
} catch {
100+
return { history: [] };
101+
}
102+
}
103+
104+
async function saveSession(session: SessionData): Promise<void> {
105+
try {
106+
const sessionPath = await getSessionPath();
107+
await fs.writeFile(sessionPath, JSON.stringify(session, null, 2));
108+
} catch (error) {
109+
console.error('Failed to save session:', error);
110+
}
111+
}
112+
113+
function convertHistoryToMessages(history: SessionData['history']): BaseMessage[] {
114+
return history.map(msg =>
115+
msg.type === 'human'
116+
? new HumanMessage(msg.content)
117+
: new AIMessage(msg.content)
118+
);
119+
}
32120

33121
export async function run() {
122+
const { question, userId, isNew } = parseArgs();
34123
const azureOpenAiEndpoint = process.env.AZURE_OPENAI_API_ENDPOINT;
35124
const burgerMcpEndpoint = process.env.BURGER_MCP_URL ?? 'http://localhost:3000/mcp';
36125

@@ -40,20 +129,25 @@ export async function run() {
40129
if (!azureOpenAiEndpoint || !burgerMcpEndpoint) {
41130
const errorMessage = 'Missing required environment variables: AZURE_OPENAI_API_ENDPOINT or BURGER_MCP_URL';
42131
console.error(errorMessage);
43-
return {
44-
status: 500,
45-
jsonBody: {
46-
error: errorMessage,
47-
},
48-
};
132+
return;
133+
}
134+
135+
let session: SessionData;
136+
if (isNew) {
137+
session = { history: [], userId };
138+
} else {
139+
session = await loadSession();
140+
if (userId && session.userId !== userId) {
141+
session.userId = userId;
142+
}
49143
}
50144

51145
const azureADTokenProvider = getAzureOpenAiTokenProvider();
52146

53147
model = new AzureChatOpenAI({
54-
// Controls randomness. 0 = deterministic, 1 = maximum randomness
55148
temperature: 0.3,
56149
azureADTokenProvider,
150+
azureOpenAIApiVersion: process.env.AZURE_OPENAI_API_VERSION,
57151
});
58152

59153
const client = new Client({
@@ -67,8 +161,10 @@ export async function run() {
67161
const tools = await loadMcpTools('burger', client);
68162
console.log(`Loaded ${tools.length} tools from Burger MCP server`);
69163

164+
console.log(`Thinking...`);
165+
70166
const prompt = ChatPromptTemplate.fromMessages([
71-
['system', agentSystemPrompt],
167+
['system', agentSystemPrompt + (session.userId ? `\n\nUser ID: ${session.userId}` : '')],
72168
['placeholder', '{chat_history}'],
73169
['human', '{input}'],
74170
['placeholder', '{agent_scratchpad}'],
@@ -82,17 +178,31 @@ export async function run() {
82178
const agentExecutor = new AgentExecutor({
83179
agent,
84180
tools,
85-
returnIntermediateSteps: true,
181+
returnIntermediateSteps: false,
182+
});
183+
184+
const chatHistory = convertHistoryToMessages(session.history);
185+
186+
const response = await agentExecutor.invoke({
187+
input: question,
188+
chat_history: chatHistory
86189
});
87190

88-
const response = await agentExecutor.invoke({ input: question });
191+
console.log('----------\n' + response.output);
192+
193+
session.history.push({ type: 'human', content: question });
194+
session.history.push({ type: 'ai', content: response.output });
195+
196+
await saveSession(session);
197+
198+
console.log('----------\nDone.');
89199

90-
console.log('Intermediate steps:', JSON.stringify(response.intermediateSteps, null, 2));
91-
console.log('Final answer:', response.output);
92200
} catch (_error: unknown) {
93201
const error = _error as Error;
94-
console.error(`Error when processing chat-post request: ${error.message}`);
202+
console.error(`Error when processing request: ${error.message}`);
95203
}
96204
}
97205

98-
run();
206+
if (process.argv[1] && process.argv[1].endsWith('agent-cli.ts') || process.argv[1].endsWith('agent-cli.js')) {
207+
run();
208+
}

packages/agent-api/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
"start:host": "func start --port 7072",
1313
"start": "concurrently npm:start:* npm:watch --raw --kill-others",
1414
"update:local-settings": "node ./scripts/update-local-settings.mjs",
15-
"postinstall": "npm run update:local-settings"
15+
"postinstall": "npm run update:local-settings",
16+
"cli": "tsx agent-cli.ts --"
1617
},
1718
"author": "Microsoft",
1819
"license": "MIT",
@@ -24,14 +25,15 @@
2425
"@langchain/core": "^0.3.18",
2526
"@langchain/langgraph": "^0.3.6",
2627
"@langchain/mcp-adapters": "^0.5.2",
27-
"@langchain/openai": "^0.6.2",
28+
"@langchain/openai": "^0.5.18",
2829
"@microsoft/ai-chat-protocol": "^1.0.0-beta.20240814.1",
2930
"dotenv": "^17.0.1",
3031
"langchain": "^0.3.6"
3132
},
3233
"devDependencies": {
3334
"@types/node": "^20",
3435
"azure-functions-core-tools": "^4",
36+
"tsx": "^4",
3537
"typescript": "^5"
3638
}
3739
}

0 commit comments

Comments
 (0)