mirror of
https://github.com/jina-ai/node-DeepResearch.git
synced 2026-03-22 07:29:35 +08:00
feat: add web server wrapper for getResponse (#6)
* feat: add web server wrapper for getResponse Co-Authored-By: Han Xiao <han.xiao@jina.ai> * chore: update package-lock.json Co-Authored-By: Han Xiao <han.xiao@jina.ai> * fix: add proper types to Express route handlers Co-Authored-By: Han Xiao <han.xiao@jina.ai> * refactor: update API endpoint path and request parameters Co-Authored-By: Han Xiao <han.xiao@jina.ai> * refactor: centralize model configurations with per-tool settings Co-Authored-By: Han Xiao <han.xiao@jina.ai> * refactor: update agent.ts to use centralized model configuration 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:
committed by
GitHub
parent
46cd8337b7
commit
ca0f780cc3
50
README.md
50
README.md
@@ -17,6 +17,54 @@ npm run dev "who will be president of US in 2028?"
|
|||||||
npm run dev "what should be jina ai strategy for 2025?"
|
npm run dev "what should be jina ai strategy for 2025?"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Web Server API
|
||||||
|
|
||||||
|
Start the server:
|
||||||
|
```bash
|
||||||
|
npm run serve
|
||||||
|
```
|
||||||
|
|
||||||
|
The server will start on http://localhost:3000 with the following endpoints:
|
||||||
|
|
||||||
|
### POST /api/v1/query
|
||||||
|
Submit a query to be answered:
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/api/v1/query \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"q": "what is the capital of France?",
|
||||||
|
"budget": 1000000,
|
||||||
|
"maxBadAttempt": 3
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"requestId": "1234567890"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### GET /api/v1/stream/:requestId
|
||||||
|
Connect to the Server-Sent Events stream to receive progress updates and the final answer:
|
||||||
|
```bash
|
||||||
|
curl -N http://localhost:3000/api/v1/stream/1234567890
|
||||||
|
```
|
||||||
|
|
||||||
|
The server will emit the following event types:
|
||||||
|
- Progress updates: Step number and budget usage
|
||||||
|
- Final answer with complete response data
|
||||||
|
- Error messages if something goes wrong
|
||||||
|
|
||||||
|
Example events:
|
||||||
|
```
|
||||||
|
data: {"type":"progress","data":"Step 1 / Budget used 10%"}
|
||||||
|
data: {"type":"progress","data":"Step 2 / Budget used 25%"}
|
||||||
|
data: {"type":"answer","data":{"action":"answer","answer":"Paris is the capital of France","references":[]}}
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart TD
|
flowchart TD
|
||||||
subgraph Inputs[System Inputs]
|
subgraph Inputs[System Inputs]
|
||||||
@@ -84,4 +132,4 @@ flowchart TD
|
|||||||
class GapQueue,ContextStore,BadStore,QuestionStore,KeywordStore,KnowledgeStore,URLStore state
|
class GapQueue,ContextStore,BadStore,QuestionStore,KeywordStore,KnowledgeStore,URLStore state
|
||||||
class OrigQuestion,TokenBudget input
|
class OrigQuestion,TokenBudget input
|
||||||
class FinalAnswer output
|
class FinalAnswer output
|
||||||
```
|
```
|
||||||
|
|||||||
960
package-lock.json
generated
960
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,8 @@
|
|||||||
"search": "npx ts-node src/test-duck.ts",
|
"search": "npx ts-node src/test-duck.ts",
|
||||||
"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"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
@@ -16,10 +17,14 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@google/generative-ai": "^0.21.0",
|
"@google/generative-ai": "^0.21.0",
|
||||||
|
"@types/cors": "^2.8.17",
|
||||||
|
"@types/express": "^5.0.0",
|
||||||
"@types/node-fetch": "^2.6.12",
|
"@types/node-fetch": "^2.6.12",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"duck-duck-scrape": "^2.2.7",
|
"duck-duck-scrape": "^2.2.7",
|
||||||
|
"express": "^4.21.2",
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
"undici": "^7.3.0"
|
"undici": "^7.3.0"
|
||||||
},
|
},
|
||||||
|
|||||||
12
src/agent.ts
12
src/agent.ts
@@ -7,7 +7,7 @@ import {rewriteQuery} from "./tools/query-rewriter";
|
|||||||
import {dedupQueries} from "./tools/dedup";
|
import {dedupQueries} from "./tools/dedup";
|
||||||
import {evaluateAnswer} from "./tools/evaluator";
|
import {evaluateAnswer} from "./tools/evaluator";
|
||||||
import {analyzeSteps} from "./tools/error-analyzer";
|
import {analyzeSteps} from "./tools/error-analyzer";
|
||||||
import {GEMINI_API_KEY, JINA_API_KEY, MODEL_NAME, SEARCH_PROVIDER, STEP_SLEEP} from "./config";
|
import {GEMINI_API_KEY, JINA_API_KEY, SEARCH_PROVIDER, STEP_SLEEP, modelConfigs} from "./config";
|
||||||
import {tokenTracker} from "./utils/token-tracker";
|
import {tokenTracker} from "./utils/token-tracker";
|
||||||
import {StepAction, SchemaProperty, ResponseSchema} from "./types";
|
import {StepAction, SchemaProperty, ResponseSchema} from "./types";
|
||||||
|
|
||||||
@@ -241,7 +241,7 @@ function removeAllLineBreaks(text: string) {
|
|||||||
return text.replace(/(\r\n|\n|\r)/gm, " ");
|
return text.replace(/(\r\n|\n|\r)/gm, " ");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getResponse(question: string, tokenBudget: number = 1_000_000, maxBadAttempts: number = 3) {
|
export async function getResponse(question: string, tokenBudget: number = 1_000_000, maxBadAttempts: number = 3) {
|
||||||
let step = 0;
|
let step = 0;
|
||||||
let totalStep = 0;
|
let totalStep = 0;
|
||||||
let badAttempts = 0;
|
let badAttempts = 0;
|
||||||
@@ -291,9 +291,9 @@ async function getResponse(question: string, tokenBudget: number = 1_000_000, ma
|
|||||||
);
|
);
|
||||||
|
|
||||||
const model = genAI.getGenerativeModel({
|
const model = genAI.getGenerativeModel({
|
||||||
model: MODEL_NAME,
|
model: modelConfigs.agent.model,
|
||||||
generationConfig: {
|
generationConfig: {
|
||||||
temperature: 0.7,
|
temperature: modelConfigs.agent.temperature,
|
||||||
responseMimeType: "application/json",
|
responseMimeType: "application/json",
|
||||||
responseSchema: getSchema(allowReflect, allowRead, allowAnswer, allowSearch)
|
responseSchema: getSchema(allowReflect, allowRead, allowAnswer, allowSearch)
|
||||||
}
|
}
|
||||||
@@ -610,9 +610,9 @@ You decided to think out of the box or cut from a completely different angle.`);
|
|||||||
);
|
);
|
||||||
|
|
||||||
const model = genAI.getGenerativeModel({
|
const model = genAI.getGenerativeModel({
|
||||||
model: MODEL_NAME,
|
model: modelConfigs.agentBeastMode.model,
|
||||||
generationConfig: {
|
generationConfig: {
|
||||||
temperature: 0.7,
|
temperature: modelConfigs.agentBeastMode.temperature,
|
||||||
responseMimeType: "application/json",
|
responseMimeType: "application/json",
|
||||||
responseSchema: getSchema(false, false, allowAnswer, false)
|
responseSchema: getSchema(false, false, allowAnswer, false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,21 @@
|
|||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
import { ProxyAgent, setGlobalDispatcher } from 'undici';
|
import { ProxyAgent, setGlobalDispatcher } from 'undici';
|
||||||
|
|
||||||
|
interface ModelConfig {
|
||||||
|
model: string;
|
||||||
|
temperature: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToolConfigs {
|
||||||
|
dedup: ModelConfig;
|
||||||
|
evaluator: ModelConfig;
|
||||||
|
errorAnalyzer: ModelConfig;
|
||||||
|
queryRewriter: ModelConfig;
|
||||||
|
agent: ModelConfig;
|
||||||
|
agentBeastMode: ModelConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
// Setup the proxy globally if present
|
// Setup the proxy globally if present
|
||||||
@@ -19,7 +34,38 @@ export const JINA_API_KEY = process.env.JINA_API_KEY as string;
|
|||||||
export const BRAVE_API_KEY = process.env.BRAVE_API_KEY as string;
|
export const BRAVE_API_KEY = process.env.BRAVE_API_KEY as string;
|
||||||
export const SEARCH_PROVIDER = BRAVE_API_KEY ? 'brave' : 'duck';
|
export const SEARCH_PROVIDER = BRAVE_API_KEY ? 'brave' : 'duck';
|
||||||
|
|
||||||
export const MODEL_NAME = 'gemini-1.5-flash';
|
const DEFAULT_MODEL = 'gemini-1.5-flash';
|
||||||
|
|
||||||
|
const defaultConfig: ModelConfig = {
|
||||||
|
model: DEFAULT_MODEL,
|
||||||
|
temperature: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
export const modelConfigs: ToolConfigs = {
|
||||||
|
dedup: {
|
||||||
|
...defaultConfig,
|
||||||
|
temperature: 0.1
|
||||||
|
},
|
||||||
|
evaluator: {
|
||||||
|
...defaultConfig
|
||||||
|
},
|
||||||
|
errorAnalyzer: {
|
||||||
|
...defaultConfig
|
||||||
|
},
|
||||||
|
queryRewriter: {
|
||||||
|
...defaultConfig,
|
||||||
|
temperature: 0.1
|
||||||
|
},
|
||||||
|
agent: {
|
||||||
|
...defaultConfig,
|
||||||
|
temperature: 0.7
|
||||||
|
},
|
||||||
|
agentBeastMode: {
|
||||||
|
...defaultConfig,
|
||||||
|
temperature: 0.7
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const STEP_SLEEP = 1000;
|
export const STEP_SLEEP = 1000;
|
||||||
|
|
||||||
if (!GEMINI_API_KEY) throw new Error("GEMINI_API_KEY not found");
|
if (!GEMINI_API_KEY) throw new Error("GEMINI_API_KEY not found");
|
||||||
|
|||||||
84
src/server.ts
Normal file
84
src/server.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import express, { Request, Response, RequestHandler } from 'express';
|
||||||
|
import cors from 'cors';
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
|
import { getResponse } from './agent';
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const port = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
// Enable CORS for localhost debugging
|
||||||
|
app.use(cors());
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Create event emitter for SSE
|
||||||
|
const eventEmitter = new EventEmitter();
|
||||||
|
|
||||||
|
// Type definitions
|
||||||
|
interface QueryRequest extends Request {
|
||||||
|
body: {
|
||||||
|
q: string;
|
||||||
|
budget?: number;
|
||||||
|
maxBadAttempt?: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StreamResponse extends Response {
|
||||||
|
write: (chunk: string) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSE endpoint for progress updates
|
||||||
|
app.get('/api/v1/stream/:requestId', ((req: Request, res: StreamResponse) => {
|
||||||
|
const requestId = req.params.requestId;
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'text/event-stream');
|
||||||
|
res.setHeader('Cache-Control', 'no-cache');
|
||||||
|
res.setHeader('Connection', 'keep-alive');
|
||||||
|
|
||||||
|
const listener = (data: any) => {
|
||||||
|
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
||||||
|
};
|
||||||
|
|
||||||
|
eventEmitter.on(`progress-${requestId}`, listener);
|
||||||
|
|
||||||
|
req.on('close', () => {
|
||||||
|
eventEmitter.removeListener(`progress-${requestId}`, listener);
|
||||||
|
});
|
||||||
|
}) as RequestHandler);
|
||||||
|
|
||||||
|
// POST endpoint to handle questions
|
||||||
|
app.post('/api/v1/query', (async (req: QueryRequest, res: Response) => {
|
||||||
|
const { q, budget, maxBadAttempt } = req.body;
|
||||||
|
if (!q) {
|
||||||
|
return res.status(400).json({ error: 'Query (q) is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestId = Date.now().toString();
|
||||||
|
res.json({ requestId });
|
||||||
|
|
||||||
|
// Store original console.log
|
||||||
|
const originalConsoleLog: typeof console.log = console.log;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Wrap getResponse to emit progress
|
||||||
|
console.log = (...args: any[]) => {
|
||||||
|
originalConsoleLog(...args);
|
||||||
|
const message = args.join(' ');
|
||||||
|
if (message.includes('Step') || message.includes('Budget used')) {
|
||||||
|
eventEmitter.emit(`progress-${requestId}`, { type: 'progress', data: message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await getResponse(q, budget, maxBadAttempt);
|
||||||
|
eventEmitter.emit(`progress-${requestId}`, { type: 'answer', data: result });
|
||||||
|
} catch (error: any) {
|
||||||
|
eventEmitter.emit(`progress-${requestId}`, { type: 'error', data: error?.message || 'Unknown error' });
|
||||||
|
} finally {
|
||||||
|
console.log = originalConsoleLog;
|
||||||
|
}
|
||||||
|
}) as RequestHandler);
|
||||||
|
|
||||||
|
app.listen(port, () => {
|
||||||
|
console.log(`Server running at http://localhost:${port}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default app;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { GoogleGenerativeAI, SchemaType } from "@google/generative-ai";
|
import { GoogleGenerativeAI, SchemaType } from "@google/generative-ai";
|
||||||
import { GEMINI_API_KEY, MODEL_NAME } from "../config";
|
import { GEMINI_API_KEY, modelConfigs } from "../config";
|
||||||
import { tokenTracker } from "../utils/token-tracker";
|
import { tokenTracker } from "../utils/token-tracker";
|
||||||
|
|
||||||
import { DedupResponse } from '../types';
|
import { DedupResponse } from '../types';
|
||||||
@@ -24,9 +24,9 @@ const responseSchema = {
|
|||||||
|
|
||||||
const genAI = new GoogleGenerativeAI(GEMINI_API_KEY);
|
const genAI = new GoogleGenerativeAI(GEMINI_API_KEY);
|
||||||
const model = genAI.getGenerativeModel({
|
const model = genAI.getGenerativeModel({
|
||||||
model: MODEL_NAME,
|
model: modelConfigs.dedup.model,
|
||||||
generationConfig: {
|
generationConfig: {
|
||||||
temperature: 0.1,
|
temperature: modelConfigs.dedup.temperature,
|
||||||
responseMimeType: "application/json",
|
responseMimeType: "application/json",
|
||||||
responseSchema: responseSchema
|
responseSchema: responseSchema
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {GoogleGenerativeAI, SchemaType} from "@google/generative-ai";
|
import {GoogleGenerativeAI, SchemaType} from "@google/generative-ai";
|
||||||
import { GEMINI_API_KEY, MODEL_NAME } from "../config";
|
import { GEMINI_API_KEY, modelConfigs } from "../config";
|
||||||
import { tokenTracker } from "../utils/token-tracker";
|
import { tokenTracker } from "../utils/token-tracker";
|
||||||
|
|
||||||
import { ErrorAnalysisResponse } from '../types';
|
import { ErrorAnalysisResponse } from '../types';
|
||||||
@@ -25,9 +25,9 @@ const responseSchema = {
|
|||||||
|
|
||||||
const genAI = new GoogleGenerativeAI(GEMINI_API_KEY);
|
const genAI = new GoogleGenerativeAI(GEMINI_API_KEY);
|
||||||
const model = genAI.getGenerativeModel({
|
const model = genAI.getGenerativeModel({
|
||||||
model: MODEL_NAME,
|
model: modelConfigs.errorAnalyzer.model,
|
||||||
generationConfig: {
|
generationConfig: {
|
||||||
temperature: 0,
|
temperature: modelConfigs.errorAnalyzer.temperature,
|
||||||
responseMimeType: "application/json",
|
responseMimeType: "application/json",
|
||||||
responseSchema: responseSchema
|
responseSchema: responseSchema
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { GoogleGenerativeAI, SchemaType } from "@google/generative-ai";
|
import { GoogleGenerativeAI, SchemaType } from "@google/generative-ai";
|
||||||
import { GEMINI_API_KEY, MODEL_NAME } from "../config";
|
import { GEMINI_API_KEY, modelConfigs } from "../config";
|
||||||
import { tokenTracker } from "../utils/token-tracker";
|
import { tokenTracker } from "../utils/token-tracker";
|
||||||
|
|
||||||
import { EvaluationResponse } from '../types';
|
import { EvaluationResponse } from '../types';
|
||||||
@@ -21,9 +21,9 @@ const responseSchema = {
|
|||||||
|
|
||||||
const genAI = new GoogleGenerativeAI(GEMINI_API_KEY);
|
const genAI = new GoogleGenerativeAI(GEMINI_API_KEY);
|
||||||
const model = genAI.getGenerativeModel({
|
const model = genAI.getGenerativeModel({
|
||||||
model: MODEL_NAME,
|
model: modelConfigs.evaluator.model,
|
||||||
generationConfig: {
|
generationConfig: {
|
||||||
temperature: 0,
|
temperature: modelConfigs.evaluator.temperature,
|
||||||
responseMimeType: "application/json",
|
responseMimeType: "application/json",
|
||||||
responseSchema: responseSchema
|
responseSchema: responseSchema
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { GoogleGenerativeAI, SchemaType } from "@google/generative-ai";
|
import { GoogleGenerativeAI, SchemaType } from "@google/generative-ai";
|
||||||
import { GEMINI_API_KEY, MODEL_NAME } from "../config";
|
import { GEMINI_API_KEY, modelConfigs } from "../config";
|
||||||
import { tokenTracker } from "../utils/token-tracker";
|
import { tokenTracker } from "../utils/token-tracker";
|
||||||
import { SearchAction } from "../types";
|
import { SearchAction } from "../types";
|
||||||
|
|
||||||
@@ -28,9 +28,9 @@ const responseSchema = {
|
|||||||
|
|
||||||
const genAI = new GoogleGenerativeAI(GEMINI_API_KEY);
|
const genAI = new GoogleGenerativeAI(GEMINI_API_KEY);
|
||||||
const model = genAI.getGenerativeModel({
|
const model = genAI.getGenerativeModel({
|
||||||
model: MODEL_NAME,
|
model: modelConfigs.queryRewriter.model,
|
||||||
generationConfig: {
|
generationConfig: {
|
||||||
temperature: 0.1,
|
temperature: modelConfigs.queryRewriter.temperature,
|
||||||
responseMimeType: "application/json",
|
responseMimeType: "application/json",
|
||||||
responseSchema: responseSchema
|
responseSchema: responseSchema
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user