Skip to content

Commit f3d7c68

Browse files
fix(agents): get exact token calculation (#22)
1 parent e1d726d commit f3d7c68

File tree

9 files changed

+257
-162
lines changed

9 files changed

+257
-162
lines changed

README.md

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ Cairo Coder is an intelligent code generation service that makes writing Cairo s
4444
- **Multiple LLM Support**: Works with OpenAI, Anthropic, and Google models
4545
- **Source-Informed Generation**: Code is generated based on Cairo documentation, ensuring correctness
4646

47-
4847
## Installation
4948

5049
There are mainly 2 ways of installing Cairo Coder - With Docker, Without Docker. Using Docker is highly recommended.
@@ -68,7 +67,6 @@ There are mainly 2 ways of installing Cairo Coder - With Docker, Without Docker.
6867
pnpm install
6968
```
7069

71-
7270
5. Inside the packages/agents package, copy the `sample.config.toml` file to a `config.toml`. For development setups, you need only fill in the following fields:
7371

7472
- `OPENAI`: Your OpenAI API key. **You only need to fill this if you wish to use OpenAI's models**.
@@ -115,45 +113,45 @@ There are mainly 2 ways of installing Cairo Coder - With Docker, Without Docker.
115113
```
116114

117115
This configuration is used by the backend and ingester services to connect to the database.
118-
Note that `POSTGRES_HOST` is set to ```"postgres"``` and `POSTGRES_PORT` to ```"5432"```, which are the container's name and port in docker-compose.yml.
116+
Note that `POSTGRES_HOST` is set to `"postgres"` and `POSTGRES_PORT` to `"5432"`, which are the container's name and port in docker-compose.yml.
119117

120118
**Important:** Make sure to use the same password, username and db's name in both files. The first file initializes the database, while the second is used by your application to connect to it.
121119

122-
123120
7. **Configure LangSmith (Optional)**
124121

125122
Cairo Coder can use LangSmith to record and monitor LLM calls. This step is optional but recommended for development and debugging.
126-
123+
127124
- Create an account at [LangSmith](https://smith.langchain.com/)
128125
- Create a new project in the LangSmith dashboard
129126
- Retrieve your API credentials
130127
- Create a `.env` file in the `packages/backend` directory with the following variables:
128+
131129
```
132-
LANGSMITH_TRACING=true
133-
LANGSMITH_ENDPOINT="https://api.smith.langchain.com"
134-
LANGSMITH_API_KEY="<your-api-key>"
130+
LANGCHAIN_TRACING=true
131+
LANGCHAIN_ENDPOINT="https://api.smith.langchain.com"
132+
LANGCHAIN_API_KEY="<your-api-key>"
135133
LANGCHAIN_PROJECT="<your-project-name>"
136134
```
137-
- Add the `.env` in an env_file section in the backend service of the docker-compose.yml
138135

139-
With this configuration, all LLM calls and chain executions will be logged to your LangSmith project, allowing you to debug, analyze, and improve the system's performance.
136+
- Add the `packages/backend/.env` in an env_file section in the backend service of the docker-compose.yml
140137

138+
With this configuration, all LLM calls and chain executions will be logged to your LangSmith project, allowing you to debug, analyze, and improve the system's performance.
141139

142-
9. Run the application using one of the following methods:
140+
8. Run the application using one of the following methods:
143141

144142
```bash
145143
docker compose up postgres backend
146144
```
147145

148-
8. The API will be available at http://localhost:3001/v1/chat/completions
146+
9. The API will be available at http://localhost:3001/v1/chat/completions
149147

150148
## Running the Ingester
151149

152150
After you have the main application running, you might need to run the ingester to process and embed documentation from various sources. The ingester is configured as a separate profile in the docker-compose file and can be executed as follows:
153151

154-
```bash
155-
docker compose up ingester
156-
```
152+
```bash
153+
docker compose up ingester
154+
```
157155

158156
Once the ingester completes its task, the vector database will be populated with embeddings from all the supported documentation sources, making them available for RAG-based code generation requests to the API.
159157

@@ -188,6 +186,7 @@ curl -X POST http://localhost:3001/v1/chat/completions \
188186
The API accepts all standard OpenAI Chat Completions parameters.
189187

190188
**Supported Parameters:**
189+
191190
- `model`: Model identifier (string)
192191
- `messages`: Array of message objects with `role` and `content`
193192
- `temperature`: Controls randomness (0-2, default: 0.7)
@@ -202,7 +201,6 @@ The API accepts all standard OpenAI Chat Completions parameters.
202201
- `user`: User identifier
203202
- `response_format`: Response format specification
204203

205-
206204
### Response Format
207205

208206
#### Standard Mode Response

packages/agents/__tests__/answerGenerator.test.ts

Lines changed: 105 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ import {
1010
import { Document } from '@langchain/core/documents';
1111
import { mockDeep, MockProxy } from 'jest-mock-extended';
1212
import { IterableReadableStream } from '@langchain/core/utils/stream';
13-
import { BaseMessage, BaseMessageChunk } from '@langchain/core/messages';
13+
import { StreamEvent } from '@langchain/core/tracers/log_stream';
1414
import { BaseLanguageModelInput } from '@langchain/core/language_models/base';
15+
import { AIMessageChunk } from '@langchain/core/messages'; // ✅ AJOUTÉ
1516

1617
// Mock the formatChatHistoryAsString utility
1718
jest.mock('../src/utils/index', () => ({
@@ -61,23 +62,21 @@ describe('AnswerGenerator', () => {
6162
testTemplate: '<test_template>Example test template</test_template>',
6263
};
6364

64-
// Mock the LLM stream method to return simulated stream
65-
mockLLM.stream.mockImplementation(
66-
(
67-
input: BaseLanguageModelInput,
68-
): Promise<IterableReadableStream<BaseMessageChunk>> => {
69-
return Promise.resolve({
70-
[Symbol.asyncIterator]: async function* () {
71-
yield {
72-
content: 'This is a test response about Cairo.',
73-
type: 'ai',
74-
name: 'AI',
75-
additional_kwargs: {},
76-
};
65+
// ✅ CHANGÉ: streamEvents au lieu de stream
66+
(mockLLM.streamEvents as any) = jest.fn().mockReturnValue({
67+
[Symbol.asyncIterator]: async function* () {
68+
yield {
69+
event: 'on_llm_stream',
70+
data: {
71+
chunk: { content: 'This is a test response about Cairo.' },
7772
},
78-
} as unknown as IterableReadableStream<BaseMessageChunk>);
73+
run_id: 'test-run-123',
74+
name: 'TestLLM',
75+
tags: [],
76+
metadata: {},
77+
} as StreamEvent;
7978
},
80-
);
79+
} as IterableReadableStream<StreamEvent>);
8180

8281
// Create the AnswerGenerator instance
8382
answerGenerator = new AnswerGenerator(mockLLM, mockConfig);
@@ -122,17 +121,20 @@ describe('AnswerGenerator', () => {
122121
const result = await answerGenerator.generate(input, retrievedDocs);
123122

124123
// Assert
125-
expect(mockLLM.stream).toHaveBeenCalled();
124+
expect(mockLLM.streamEvents).toHaveBeenCalled(); // ✅ CHANGÉ: streamEvents
126125

127126
// Collect the stream results
128-
const messages: any[] = [];
129-
for await (const message of result) {
130-
messages.push(message);
127+
const events: StreamEvent[] = [];
128+
for await (const event of result) {
129+
events.push(event);
131130
}
132131

133-
// Check that we got the expected message
134-
expect(messages.length).toBe(1);
135-
expect(messages[0].content).toBe('This is a test response about Cairo.');
132+
// Check that we got the expected events
133+
expect(events.length).toBe(1);
134+
expect(events[0].event).toBe('on_llm_stream');
135+
expect(events[0].data.chunk.content).toBe(
136+
'This is a test response about Cairo.',
137+
);
136138
});
137139

138140
it('should include contract template when query is contract-related', async () => {
@@ -169,27 +171,39 @@ describe('AnswerGenerator', () => {
169171
processedQuery,
170172
};
171173

172-
// Create a spy on the LLM stream method to capture the actual prompt
174+
// Create a spy on the LLM streamEvents method to capture the actual prompt
173175
let capturedPrompt: string | null = null;
174-
mockLLM.stream.mockImplementation((prompt) => {
175-
capturedPrompt = prompt as string;
176-
return Promise.resolve({
177-
[Symbol.asyncIterator]: async function* () {
178-
yield {
179-
content: 'This is a test response about Cairo contracts.',
180-
type: 'ai',
181-
name: 'AI',
182-
additional_kwargs: {},
183-
};
184-
},
185-
} as unknown as IterableReadableStream<BaseMessageChunk>);
186-
});
176+
const mockReturnValue = {
177+
[Symbol.asyncIterator]: async function* () {
178+
yield {
179+
event: 'on_llm_stream',
180+
data: {
181+
chunk: {
182+
content: 'This is a test response about Cairo contracts.',
183+
},
184+
},
185+
run_id: 'test-run-123',
186+
name: 'TestLLM',
187+
tags: [],
188+
metadata: {},
189+
} as StreamEvent;
190+
},
191+
} as IterableReadableStream<StreamEvent>;
192+
193+
(mockLLM.streamEvents as any) = jest
194+
.fn()
195+
.mockImplementation((...args) => {
196+
capturedPrompt = args[0] as string;
197+
return mockReturnValue;
198+
});
187199

188200
// Act
189201
await answerGenerator.generate(input, retrievedDocs);
190202

191203
// Assert
192-
expect(capturedPrompt).toContain(
204+
expect(capturedPrompt).toBeDefined();
205+
const promptString = JSON.stringify(capturedPrompt);
206+
expect(promptString).toContain(
193207
'<contract_template>Example template</contract_template>',
194208
);
195209
});
@@ -229,27 +243,40 @@ describe('AnswerGenerator', () => {
229243
processedQuery,
230244
};
231245

232-
// Create a spy on the LLM stream method to capture the actual prompt
246+
// Create a spy on the LLM streamEvents method to capture the actual prompt
233247
let capturedPrompt: string | null = null;
234-
mockLLM.stream.mockImplementation((prompt) => {
235-
capturedPrompt = prompt as string;
236-
return Promise.resolve({
237-
[Symbol.asyncIterator]: async function* () {
238-
yield {
239-
content: 'This is a test response about testing Cairo contracts.',
240-
type: 'ai',
241-
name: 'AI',
242-
additional_kwargs: {},
243-
};
244-
},
245-
} as unknown as IterableReadableStream<BaseMessageChunk>);
246-
});
248+
const mockReturnValue = {
249+
[Symbol.asyncIterator]: async function* () {
250+
yield {
251+
event: 'on_llm_stream',
252+
data: {
253+
chunk: {
254+
content:
255+
'This is a test response about testing Cairo contracts.',
256+
},
257+
},
258+
run_id: 'test-run-123',
259+
name: 'TestLLM',
260+
tags: [],
261+
metadata: {},
262+
} as StreamEvent;
263+
},
264+
} as IterableReadableStream<StreamEvent>;
265+
266+
(mockLLM.streamEvents as any) = jest
267+
.fn()
268+
.mockImplementation((...args) => {
269+
capturedPrompt = args[0] as string;
270+
return mockReturnValue;
271+
});
247272

248273
// Act
249274
await answerGenerator.generate(input, retrievedDocs);
250275

251276
// Assert
252-
expect(capturedPrompt).toContain(
277+
expect(capturedPrompt).toBeDefined();
278+
const promptString = JSON.stringify(capturedPrompt);
279+
expect(promptString).toContain(
253280
'<test_template>Example test template</test_template>',
254281
);
255282
});
@@ -273,27 +300,37 @@ describe('AnswerGenerator', () => {
273300
processedQuery,
274301
};
275302

276-
// Create a spy on the LLM stream method to capture the actual prompt
303+
// Create a spy on the LLM streamEvents method to capture the actual prompt
277304
let capturedPrompt: string | null = null;
278-
mockLLM.stream.mockImplementation((prompt) => {
279-
capturedPrompt = prompt as string;
280-
return Promise.resolve({
281-
[Symbol.asyncIterator]: async function* () {
282-
yield {
283-
content: 'I cannot find any relevant information.',
284-
type: 'ai',
285-
name: 'AI',
286-
additional_kwargs: {},
287-
};
288-
},
289-
} as unknown as IterableReadableStream<BaseMessageChunk>);
290-
});
305+
const mockReturnValue = {
306+
[Symbol.asyncIterator]: async function* () {
307+
yield {
308+
event: 'on_llm_stream',
309+
data: {
310+
chunk: { content: 'I cannot find any relevant information.' },
311+
},
312+
run_id: 'test-run-123',
313+
name: 'TestLLM',
314+
tags: [],
315+
metadata: {},
316+
} as StreamEvent;
317+
},
318+
} as IterableReadableStream<StreamEvent>;
319+
320+
(mockLLM.streamEvents as any) = jest
321+
.fn()
322+
.mockImplementation((...args) => {
323+
capturedPrompt = args[0] as string;
324+
return mockReturnValue;
325+
});
291326

292327
// Act
293328
await answerGenerator.generate(input, retrievedDocs);
294329

295330
// Assert
296-
expect(capturedPrompt).toContain(
331+
expect(capturedPrompt).toBeDefined();
332+
const promptString = JSON.stringify(capturedPrompt);
333+
expect(promptString).toContain(
297334
'Sorry, no relevant information was found.',
298335
);
299336
});

packages/agents/__tests__/ragPipeline.test.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { StreamEvent } from '@langchain/core/tracers/log_stream';
12
import { RagPipeline } from '../src/core/pipeline/ragPipeline';
23
import { QueryProcessor } from '../src/core/pipeline/queryProcessor';
34
import { DocumentRetriever } from '../src/core/pipeline/documentRetriever';
@@ -13,8 +14,8 @@ import {
1314
import { Document } from '@langchain/core/documents';
1415
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
1516
import { IterableReadableStream } from '@langchain/core/utils/stream';
16-
import { BaseMessage, AIMessage } from '@langchain/core/messages';
1717
import { mockDeep, MockProxy } from 'jest-mock-extended';
18+
import { AIMessage } from '@langchain/core/messages';
1819
import EventEmitter from 'events';
1920

2021
// Mock the dependencies at the module level
@@ -133,10 +134,20 @@ describe('RagPipeline', () => {
133134
// Create a mock response stream
134135
const mockStream = {
135136
[Symbol.asyncIterator]: async function* () {
136-
yield new AIMessage('This is a test answer about Cairo contracts.');
137+
yield {
138+
event: 'on_llm_stream',
139+
data: {
140+
chunk: {
141+
content: 'This is a test answer about Cairo contracts.',
142+
},
143+
},
144+
run_id: 'test-run-123',
145+
name: 'TestLLM',
146+
tags: [],
147+
metadata: {},
148+
} as StreamEvent;
137149
},
138-
} as IterableReadableStream<BaseMessage>;
139-
150+
} as IterableReadableStream<StreamEvent>;
140151
// Setup mock behavior
141152
mockQueryProcessor.process.mockResolvedValue(processedQuery);
142153
mockDocumentRetriever.retrieve.mockResolvedValue(retrievedDocs);

0 commit comments

Comments
 (0)