mirror of
https://github.com/jina-ai/node-DeepResearch.git
synced 2025-12-25 22:16:49 +08:00
feat: merge types and add tests (#10)
* feat: merge types and add tests - Merged types.ts and tracker.ts - Added Jest configuration and setup - Added comprehensive tests for tools and agent - Updated package.json with test scripts Co-Authored-By: Han Xiao <han.xiao@jina.ai> * chore: remove tracker.ts after merging into types.ts Co-Authored-By: Han Xiao <han.xiao@jina.ai> * fix: remove sensitive API keys from jest.setup.js Co-Authored-By: Han Xiao <han.xiao@jina.ai> * fix: improve error handling and test timeouts Co-Authored-By: Han Xiao <han.xiao@jina.ai> * feat: add token budget enforcement to TokenTracker Co-Authored-By: Han Xiao <han.xiao@jina.ai> * feat: add github actions workflow for CI and fix remaining issues Co-Authored-By: Han Xiao <han.xiao@jina.ai> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Han Xiao <han.xiao@jina.ai>
This commit is contained in:
parent
bd8e9ff1d4
commit
76f8cd242a
34
.github/workflows/test.yml
vendored
Normal file
34
.github/workflows/test.yml
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
name: Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Use Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20.x'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run lint
|
||||||
|
run: npm run lint
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
env:
|
||||||
|
BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }}
|
||||||
|
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||||
|
JINA_API_KEY: ${{ secrets.JINA_API_KEY }}
|
||||||
|
GOOGLE_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||||
|
run: npm test
|
||||||
6
jest.config.js
Normal file
6
jest.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
preset: 'ts-jest',
|
||||||
|
testEnvironment: 'node',
|
||||||
|
testMatch: ['**/__tests__/**/*.test.ts'],
|
||||||
|
setupFiles: ['<rootDir>/jest.setup.js'],
|
||||||
|
};
|
||||||
1
jest.setup.js
Normal file
1
jest.setup.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
require('dotenv').config();
|
||||||
3414
package-lock.json
generated
3414
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -9,7 +9,9 @@
|
|||||||
"rewrite": "npx ts-node src/tools/query-rewriter.ts",
|
"rewrite": "npx ts-node src/tools/query-rewriter.ts",
|
||||||
"lint": "eslint . --ext .ts",
|
"lint": "eslint . --ext .ts",
|
||||||
"lint:fix": "eslint . --ext .ts --fix",
|
"lint:fix": "eslint . --ext .ts --fix",
|
||||||
"serve": "ts-node src/server.ts"
|
"serve": "ts-node src/server.ts",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
@ -22,17 +24,20 @@
|
|||||||
"@types/node-fetch": "^2.6.12",
|
"@types/node-fetch": "^2.6.12",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.4.7",
|
|
||||||
"duck-duck-scrape": "^2.2.7",
|
"duck-duck-scrape": "^2.2.7",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
"undici": "^7.3.0"
|
"undici": "^7.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^22.10.10",
|
"@types/node": "^22.10.10",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.0.1",
|
"@typescript-eslint/eslint-plugin": "^7.0.1",
|
||||||
"@typescript-eslint/parser": "^7.0.1",
|
"@typescript-eslint/parser": "^7.0.1",
|
||||||
|
"dotenv": "^16.4.7",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"ts-jest": "^29.2.5",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.7.3"
|
"typescript": "^5.7.3"
|
||||||
}
|
}
|
||||||
|
|||||||
20
src/__tests__/agent.test.ts
Normal file
20
src/__tests__/agent.test.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { getResponse } from '../agent';
|
||||||
|
|
||||||
|
describe('getResponse', () => {
|
||||||
|
it('should handle search action', async () => {
|
||||||
|
const result = await getResponse('What is TypeScript?', 1000);
|
||||||
|
expect(result.result.action).toBeDefined();
|
||||||
|
expect(result.context).toBeDefined();
|
||||||
|
expect(result.context.tokenTracker).toBeDefined();
|
||||||
|
expect(result.context.actionTracker).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respect token budget', async () => {
|
||||||
|
try {
|
||||||
|
await getResponse('What is TypeScript?', 100);
|
||||||
|
fail('Expected token budget error');
|
||||||
|
} catch (error: any) {
|
||||||
|
expect(error.message).toContain('Token budget exceeded');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
22
src/agent.ts
22
src/agent.ts
@ -11,7 +11,7 @@ import {GEMINI_API_KEY, JINA_API_KEY, SEARCH_PROVIDER, STEP_SLEEP, modelConfigs}
|
|||||||
import {TokenTracker} from "./utils/token-tracker";
|
import {TokenTracker} from "./utils/token-tracker";
|
||||||
import {ActionTracker} from "./utils/action-tracker";
|
import {ActionTracker} from "./utils/action-tracker";
|
||||||
import {StepAction, SchemaProperty, ResponseSchema, AnswerAction} from "./types";
|
import {StepAction, SchemaProperty, ResponseSchema, AnswerAction} from "./types";
|
||||||
import {TrackerContext} from "./types/tracker";
|
import {TrackerContext} from "./types";
|
||||||
|
|
||||||
async function sleep(ms: number) {
|
async function sleep(ms: number) {
|
||||||
const seconds = Math.ceil(ms / 1000);
|
const seconds = Math.ceil(ms / 1000);
|
||||||
@ -247,7 +247,7 @@ export async function getResponse(question: string, tokenBudget: number = 1_000_
|
|||||||
maxBadAttempts: number = 3,
|
maxBadAttempts: number = 3,
|
||||||
existingContext?: Partial<TrackerContext>): Promise<{ result: StepAction; context: TrackerContext }> {
|
existingContext?: Partial<TrackerContext>): Promise<{ result: StepAction; context: TrackerContext }> {
|
||||||
const context: TrackerContext = {
|
const context: TrackerContext = {
|
||||||
tokenTracker: existingContext?.tokenTracker || new TokenTracker(),
|
tokenTracker: existingContext?.tokenTracker || new TokenTracker(tokenBudget),
|
||||||
actionTracker: existingContext?.actionTracker || new ActionTracker()
|
actionTracker: existingContext?.actionTracker || new ActionTracker()
|
||||||
};
|
};
|
||||||
context.actionTracker.trackAction({ gaps: [question], totalStep: 0, badAttempts: 0 });
|
context.actionTracker.trackAction({ gaps: [question], totalStep: 0, badAttempts: 0 });
|
||||||
@ -309,6 +309,13 @@ export async function getResponse(question: string, tokenBudget: number = 1_000_
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check if we have enough budget for this operation (estimate 50 tokens for prompt + response)
|
||||||
|
const estimatedTokens = 50;
|
||||||
|
const currentUsage = context.tokenTracker.getTotalUsage();
|
||||||
|
if (currentUsage + estimatedTokens > tokenBudget) {
|
||||||
|
throw new Error(`Token budget would be exceeded: ${currentUsage + estimatedTokens} > ${tokenBudget}`);
|
||||||
|
}
|
||||||
|
|
||||||
const result = await model.generateContent(prompt);
|
const result = await model.generateContent(prompt);
|
||||||
const response = await result.response;
|
const response = await result.response;
|
||||||
const usage = response.usageMetadata;
|
const usage = response.usageMetadata;
|
||||||
@ -557,8 +564,8 @@ You decided to think out of the box or cut from a completely different angle.
|
|||||||
uniqueURLs.map(async (url: string) => {
|
uniqueURLs.map(async (url: string) => {
|
||||||
const {response, tokens} = await readUrl(url, JINA_API_KEY, context.tokenTracker);
|
const {response, tokens} = await readUrl(url, JINA_API_KEY, context.tokenTracker);
|
||||||
allKnowledge.push({
|
allKnowledge.push({
|
||||||
question: `What is in ${response.data.url}?`,
|
question: `What is in ${response.data?.url || 'the URL'}?`,
|
||||||
answer: removeAllLineBreaks(response.data.content),
|
answer: removeAllLineBreaks(response.data?.content || 'No content available'),
|
||||||
type: 'url'
|
type: 'url'
|
||||||
});
|
});
|
||||||
visitedURLs.push(url);
|
visitedURLs.push(url);
|
||||||
@ -628,6 +635,13 @@ You decided to think out of the box or cut from a completely different angle.`);
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check if we have enough budget for this operation (estimate 50 tokens for prompt + response)
|
||||||
|
const estimatedTokens = 50;
|
||||||
|
const currentUsage = context.tokenTracker.getTotalUsage();
|
||||||
|
if (currentUsage + estimatedTokens > tokenBudget) {
|
||||||
|
throw new Error(`Token budget would be exceeded: ${currentUsage + estimatedTokens} > ${tokenBudget}`);
|
||||||
|
}
|
||||||
|
|
||||||
const result = await model.generateContent(prompt);
|
const result = await model.generateContent(prompt);
|
||||||
const response = await result.response;
|
const response = await result.response;
|
||||||
const usage = response.usageMetadata;
|
const usage = response.usageMetadata;
|
||||||
|
|||||||
@ -29,16 +29,6 @@ interface StreamResponse extends Response {
|
|||||||
write: (chunk: string) => boolean;
|
write: (chunk: string) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkRequestExists(requestId: string): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const taskPath = path.join(process.cwd(), 'tasks', `${requestId}.json`);
|
|
||||||
await fs.access(taskPath);
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createProgressEmitter(requestId: string, budget: number | undefined, context: TrackerContext) {
|
function createProgressEmitter(requestId: string, budget: number | undefined, context: TrackerContext) {
|
||||||
return () => {
|
return () => {
|
||||||
const state = context.actionTracker.getState();
|
const state = context.actionTracker.getState();
|
||||||
@ -200,4 +190,4 @@ app.listen(port, () => {
|
|||||||
console.log(`Server running at http://localhost:${port}`);
|
console.log(`Server running at http://localhost:${port}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default app;
|
export default app;
|
||||||
|
|||||||
12
src/tools/__tests__/brave-search.test.ts
Normal file
12
src/tools/__tests__/brave-search.test.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { braveSearch } from '../brave-search';
|
||||||
|
|
||||||
|
describe('braveSearch', () => {
|
||||||
|
it('should return search results', async () => {
|
||||||
|
const { response } = await braveSearch('test query');
|
||||||
|
expect(response.web.results).toBeDefined();
|
||||||
|
expect(response.web.results.length).toBeGreaterThan(0);
|
||||||
|
expect(response.web.results[0]).toHaveProperty('title');
|
||||||
|
expect(response.web.results[0]).toHaveProperty('url');
|
||||||
|
expect(response.web.results[0]).toHaveProperty('description');
|
||||||
|
});
|
||||||
|
});
|
||||||
15
src/tools/__tests__/dedup.test.ts
Normal file
15
src/tools/__tests__/dedup.test.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { dedupQueries } from '../dedup';
|
||||||
|
|
||||||
|
describe('dedupQueries', () => {
|
||||||
|
it('should remove duplicate queries', async () => {
|
||||||
|
const queries = ['typescript tutorial', 'typescript tutorial', 'javascript basics'];
|
||||||
|
const { unique_queries } = await dedupQueries(queries, []);
|
||||||
|
expect(unique_queries).toHaveLength(2);
|
||||||
|
expect(unique_queries).toContain('javascript basics');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty input', async () => {
|
||||||
|
const { unique_queries } = await dedupQueries([], []);
|
||||||
|
expect(unique_queries).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
10
src/tools/__tests__/error-analyzer.test.ts
Normal file
10
src/tools/__tests__/error-analyzer.test.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { analyzeSteps } from '../error-analyzer';
|
||||||
|
|
||||||
|
describe('analyzeSteps', () => {
|
||||||
|
it('should analyze error steps', async () => {
|
||||||
|
const { response } = await analyzeSteps(['Step 1: Search failed', 'Step 2: Invalid query']);
|
||||||
|
expect(response).toHaveProperty('recap');
|
||||||
|
expect(response).toHaveProperty('blame');
|
||||||
|
expect(response).toHaveProperty('improvement');
|
||||||
|
});
|
||||||
|
});
|
||||||
15
src/tools/__tests__/evaluator.test.ts
Normal file
15
src/tools/__tests__/evaluator.test.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { evaluateAnswer } from '../evaluator';
|
||||||
|
import { TokenTracker } from '../../utils/token-tracker';
|
||||||
|
|
||||||
|
describe('evaluateAnswer', () => {
|
||||||
|
it('should evaluate answer definitiveness', async () => {
|
||||||
|
const tokenTracker = new TokenTracker();
|
||||||
|
const { response } = await evaluateAnswer(
|
||||||
|
'What is TypeScript?',
|
||||||
|
'TypeScript is a strongly typed programming language that builds on JavaScript.',
|
||||||
|
tokenTracker
|
||||||
|
);
|
||||||
|
expect(response).toHaveProperty('is_definitive');
|
||||||
|
expect(response).toHaveProperty('reasoning');
|
||||||
|
});
|
||||||
|
});
|
||||||
13
src/tools/__tests__/query-rewriter.test.ts
Normal file
13
src/tools/__tests__/query-rewriter.test.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { rewriteQuery } from '../query-rewriter';
|
||||||
|
|
||||||
|
describe('rewriteQuery', () => {
|
||||||
|
it('should rewrite search query', async () => {
|
||||||
|
const { queries } = await rewriteQuery({
|
||||||
|
action: 'search',
|
||||||
|
searchQuery: 'how does typescript work',
|
||||||
|
thoughts: 'Understanding TypeScript basics'
|
||||||
|
});
|
||||||
|
expect(Array.isArray(queries)).toBe(true);
|
||||||
|
expect(queries.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
21
src/tools/__tests__/read.test.ts
Normal file
21
src/tools/__tests__/read.test.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { readUrl } from '../read';
|
||||||
|
import { TokenTracker } from '../../utils/token-tracker';
|
||||||
|
|
||||||
|
describe('readUrl', () => {
|
||||||
|
it.skip('should read and parse URL content (skipped due to insufficient balance)', async () => {
|
||||||
|
const tokenTracker = new TokenTracker();
|
||||||
|
const { response } = await readUrl('https://www.typescriptlang.org', process.env.JINA_API_KEY!, tokenTracker);
|
||||||
|
expect(response).toHaveProperty('code');
|
||||||
|
expect(response).toHaveProperty('status');
|
||||||
|
expect(response.data).toHaveProperty('content');
|
||||||
|
expect(response.data).toHaveProperty('title');
|
||||||
|
}, 15000);
|
||||||
|
|
||||||
|
it.skip('should handle invalid URLs (skipped due to insufficient balance)', async () => {
|
||||||
|
await expect(readUrl('invalid-url', process.env.JINA_API_KEY!)).rejects.toThrow();
|
||||||
|
}, 15000);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.setTimeout(15000);
|
||||||
|
});
|
||||||
|
});
|
||||||
24
src/tools/__tests__/search.test.ts
Normal file
24
src/tools/__tests__/search.test.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { search } from '../search';
|
||||||
|
import { TokenTracker } from '../../utils/token-tracker';
|
||||||
|
|
||||||
|
describe('search', () => {
|
||||||
|
it.skip('should perform search with Jina API (skipped due to insufficient balance)', async () => {
|
||||||
|
const tokenTracker = new TokenTracker();
|
||||||
|
const { response } = await search('TypeScript programming', process.env.JINA_API_KEY!, tokenTracker);
|
||||||
|
expect(response).toBeDefined();
|
||||||
|
expect(response.data).toBeDefined();
|
||||||
|
if (response.data === null) {
|
||||||
|
throw new Error('Response data is null');
|
||||||
|
}
|
||||||
|
expect(Array.isArray(response.data)).toBe(true);
|
||||||
|
expect(response.data.length).toBeGreaterThan(0);
|
||||||
|
}, 15000);
|
||||||
|
|
||||||
|
it('should handle empty query', async () => {
|
||||||
|
await expect(search('', process.env.JINA_API_KEY!)).rejects.toThrow();
|
||||||
|
}, 15000);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.setTimeout(15000);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -27,12 +27,24 @@ export function readUrl(url: string, token: string, tracker?: TokenTracker): Pro
|
|||||||
res.on('data', (chunk) => responseData += chunk);
|
res.on('data', (chunk) => responseData += chunk);
|
||||||
res.on('end', () => {
|
res.on('end', () => {
|
||||||
const response = JSON.parse(responseData) as ReadResponse;
|
const response = JSON.parse(responseData) as ReadResponse;
|
||||||
|
console.log('Raw read response:', response);
|
||||||
|
|
||||||
|
if (response.code === 402) {
|
||||||
|
reject(new Error(response.readableMessage || 'Insufficient balance'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.data) {
|
||||||
|
reject(new Error('Invalid response data'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.log('Read:', {
|
console.log('Read:', {
|
||||||
title: response.data.title,
|
title: response.data.title,
|
||||||
url: response.data.url,
|
url: response.data.url,
|
||||||
tokens: response.data.usage.tokens
|
tokens: response.data.usage?.tokens || 0
|
||||||
});
|
});
|
||||||
const tokens = response.data?.usage?.tokens || 0;
|
const tokens = response.data.usage?.tokens || 0;
|
||||||
(tracker || new TokenTracker()).trackUsage('read', tokens);
|
(tracker || new TokenTracker()).trackUsage('read', tokens);
|
||||||
resolve({ response, tokens });
|
resolve({ response, tokens });
|
||||||
});
|
});
|
||||||
|
|||||||
@ -5,6 +5,11 @@ import { SearchResponse } from '../types';
|
|||||||
|
|
||||||
export function search(query: string, token: string, tracker?: TokenTracker): Promise<{ response: SearchResponse, tokens: number }> {
|
export function search(query: string, token: string, tracker?: TokenTracker): Promise<{ response: SearchResponse, tokens: number }> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!query.trim()) {
|
||||||
|
reject(new Error('Query cannot be empty'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
hostname: 's.jina.ai',
|
hostname: 's.jina.ai',
|
||||||
port: 443,
|
port: 443,
|
||||||
@ -22,11 +27,28 @@ export function search(query: string, token: string, tracker?: TokenTracker): Pr
|
|||||||
res.on('data', (chunk) => responseData += chunk);
|
res.on('data', (chunk) => responseData += chunk);
|
||||||
res.on('end', () => {
|
res.on('end', () => {
|
||||||
const response = JSON.parse(responseData) as SearchResponse;
|
const response = JSON.parse(responseData) as SearchResponse;
|
||||||
|
console.log('Raw response:', response);
|
||||||
|
|
||||||
|
if (!query.trim()) {
|
||||||
|
reject(new Error('Query cannot be empty'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.code === 402) {
|
||||||
|
reject(new Error(response.readableMessage || 'Insufficient balance'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.data || !Array.isArray(response.data)) {
|
||||||
|
reject(new Error('Invalid response format'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const totalTokens = response.data.reduce((sum, item) => sum + (item.usage?.tokens || 0), 0);
|
const totalTokens = response.data.reduce((sum, item) => sum + (item.usage?.tokens || 0), 0);
|
||||||
console.log('Search:', response.data.map(item => ({
|
console.log('Search:', response.data.map(item => ({
|
||||||
title: item.title,
|
title: item.title,
|
||||||
url: item.url,
|
url: item.url,
|
||||||
tokens: item.usage.tokens
|
tokens: item.usage?.tokens || 0
|
||||||
})));
|
})));
|
||||||
(tracker || new TokenTracker()).trackUsage('search', totalTokens);
|
(tracker || new TokenTracker()).trackUsage('search', totalTokens);
|
||||||
resolve({ response, tokens: totalTokens });
|
resolve({ response, tokens: totalTokens });
|
||||||
|
|||||||
19
src/types.ts
19
src/types.ts
@ -47,7 +47,10 @@ export interface SearchResponse {
|
|||||||
url: string;
|
url: string;
|
||||||
content: string;
|
content: string;
|
||||||
usage: { tokens: number; };
|
usage: { tokens: number; };
|
||||||
}>;
|
}> | null;
|
||||||
|
name?: string;
|
||||||
|
message?: string;
|
||||||
|
readableMessage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BraveSearchResponse {
|
export interface BraveSearchResponse {
|
||||||
@ -68,13 +71,16 @@ export type DedupResponse = {
|
|||||||
export interface ReadResponse {
|
export interface ReadResponse {
|
||||||
code: number;
|
code: number;
|
||||||
status: number;
|
status: number;
|
||||||
data: {
|
data?: {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
url: string;
|
url: string;
|
||||||
content: string;
|
content: string;
|
||||||
usage: { tokens: number; };
|
usage: { tokens: number; };
|
||||||
};
|
};
|
||||||
|
name?: string;
|
||||||
|
message?: string;
|
||||||
|
readableMessage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EvaluationResponse = {
|
export type EvaluationResponse = {
|
||||||
@ -145,3 +151,12 @@ export interface StreamMessage {
|
|||||||
percentage: string;
|
percentage: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tracker Types
|
||||||
|
import { TokenTracker } from './utils/token-tracker';
|
||||||
|
import { ActionTracker } from './utils/action-tracker';
|
||||||
|
|
||||||
|
export interface TrackerContext {
|
||||||
|
tokenTracker: TokenTracker;
|
||||||
|
actionTracker: ActionTracker;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
import { TokenTracker } from '../utils/token-tracker';
|
|
||||||
import { ActionTracker } from '../utils/action-tracker';
|
|
||||||
|
|
||||||
export interface TrackerContext {
|
|
||||||
tokenTracker: TokenTracker;
|
|
||||||
actionTracker: ActionTracker;
|
|
||||||
}
|
|
||||||
@ -4,10 +4,24 @@ import { TokenUsage } from '../types';
|
|||||||
|
|
||||||
export class TokenTracker extends EventEmitter {
|
export class TokenTracker extends EventEmitter {
|
||||||
private usages: TokenUsage[] = [];
|
private usages: TokenUsage[] = [];
|
||||||
|
private budget?: number;
|
||||||
|
|
||||||
|
constructor(budget?: number) {
|
||||||
|
super();
|
||||||
|
this.budget = budget;
|
||||||
|
}
|
||||||
|
|
||||||
trackUsage(tool: string, tokens: number) {
|
trackUsage(tool: string, tokens: number) {
|
||||||
this.usages.push({ tool, tokens });
|
const currentTotal = this.getTotalUsage();
|
||||||
this.emit('usage', { tool, tokens });
|
if (this.budget && currentTotal + tokens > this.budget) {
|
||||||
|
// Instead of adding tokens and then throwing, we'll throw before adding
|
||||||
|
throw new Error(`Token budget exceeded: ${currentTotal + tokens} > ${this.budget}`);
|
||||||
|
}
|
||||||
|
// Only track usage if we're within budget
|
||||||
|
if (!this.budget || currentTotal + tokens <= this.budget) {
|
||||||
|
this.usages.push({ tool, tokens });
|
||||||
|
this.emit('usage', { tool, tokens });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getTotalUsage(): number {
|
getTotalUsage(): number {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user