feat: default locale Russian, geo determines language for other countries
- localization-svc: defaultLocale ru, resolveLocale only by geo - web-svc: DEFAULT_LOCALE ru, layout lang=ru, embeddedTranslations fallback ru - countryToLocale: default ru when no country or unknown country Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
106
services/master-agents-svc/src/lib/agent/master.ts
Normal file
106
services/master-agents-svc/src/lib/agent/master.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import type { BaseLLM } from '../models/base/llm.js';
|
||||
import type { Message } from '../types.js';
|
||||
import { getTools, getTool, getToolSchemasForLLM } from '../tools/registry.js';
|
||||
import type { AgentContext } from '../tools/types.js';
|
||||
import { MASTER_SYSTEM_PROMPT } from '../prompts/master.js';
|
||||
|
||||
export type MasterAgentInput = {
|
||||
task: string;
|
||||
chatHistory?: [string, string][];
|
||||
context: AgentContext;
|
||||
maxSteps?: number;
|
||||
};
|
||||
|
||||
export type MasterAgentOutput = {
|
||||
content: string;
|
||||
steps: number;
|
||||
}
|
||||
|
||||
export async function runMasterAgent(
|
||||
llm: BaseLLM,
|
||||
input: MasterAgentInput,
|
||||
): Promise<MasterAgentOutput> {
|
||||
const maxSteps = input.maxSteps ?? 15;
|
||||
const tools = getTools();
|
||||
const toolSchemas = getToolSchemasForLLM();
|
||||
|
||||
const historyStr =
|
||||
input.chatHistory?.length
|
||||
? input.chatHistory
|
||||
.slice(-6)
|
||||
.map(([role, content]) => `${role === 'human' ? 'User' : 'Assistant'}: ${content}`)
|
||||
.join('\n')
|
||||
: '';
|
||||
|
||||
const userContent = historyStr
|
||||
? `<recent_conversation>\n${historyStr}\n</recent_conversation>\n\n<current_task>\n${input.task}\n</current_task>`
|
||||
: input.task;
|
||||
|
||||
const messages: Message[] = [
|
||||
{ role: 'system', content: MASTER_SYSTEM_PROMPT },
|
||||
{ role: 'user', content: userContent },
|
||||
];
|
||||
|
||||
let steps = 0;
|
||||
|
||||
while (steps < maxSteps) {
|
||||
steps += 1;
|
||||
|
||||
const result = await llm.generateText({
|
||||
messages,
|
||||
tools: toolSchemas,
|
||||
});
|
||||
|
||||
if (result.toolCalls.length === 0) {
|
||||
return {
|
||||
content: result.content.trim() || 'Нет ответа.',
|
||||
steps,
|
||||
};
|
||||
}
|
||||
|
||||
messages.push({
|
||||
role: 'assistant',
|
||||
content: result.content,
|
||||
tool_calls: result.toolCalls,
|
||||
});
|
||||
|
||||
for (const tc of result.toolCalls) {
|
||||
const tool = getTool(tc.name);
|
||||
if (!tool) {
|
||||
messages.push({
|
||||
role: 'tool',
|
||||
id: tc.id,
|
||||
name: tc.name,
|
||||
content: JSON.stringify({ error: `Unknown tool: ${tc.name}` }),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
let toolResult: string;
|
||||
try {
|
||||
toolResult = await tool.execute(tc.arguments, input.context);
|
||||
} catch (err) {
|
||||
toolResult = JSON.stringify({
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
|
||||
messages.push({
|
||||
role: 'tool',
|
||||
id: tc.id,
|
||||
name: tc.name,
|
||||
content: toolResult.length > 12000 ? toolResult.slice(0, 12000) + '\n...[truncated]' : toolResult,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const finalResult = await llm.generateText({
|
||||
messages,
|
||||
tools: [],
|
||||
});
|
||||
|
||||
return {
|
||||
content: finalResult.content.trim() || 'Достигнут лимит шагов. Попробуйте уточнить запрос.',
|
||||
steps,
|
||||
};
|
||||
}
|
||||
36
services/master-agents-svc/src/lib/config.ts
Normal file
36
services/master-agents-svc/src/lib/config.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
|
||||
export type ConfigModelProvider = {
|
||||
id: string;
|
||||
type: string;
|
||||
chatModels: { key: string; name: string }[];
|
||||
config: Record<string, unknown>;
|
||||
};
|
||||
|
||||
function getConfigPath(): string {
|
||||
return path.join(
|
||||
process.env.DATA_DIR ? path.resolve(process.cwd(), process.env.DATA_DIR) : process.cwd(),
|
||||
'data',
|
||||
'config.json',
|
||||
);
|
||||
}
|
||||
|
||||
let cached: ConfigModelProvider[] | null = null;
|
||||
|
||||
export function getProviderById(id: string): ConfigModelProvider | undefined {
|
||||
if (cached === null) {
|
||||
const p = getConfigPath();
|
||||
if (!fs.existsSync(p)) {
|
||||
cached = [];
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const raw = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
||||
cached = raw.modelProviders ?? [];
|
||||
} catch {
|
||||
cached = [];
|
||||
}
|
||||
}
|
||||
return cached?.find((x) => x.id === id);
|
||||
}
|
||||
28
services/master-agents-svc/src/lib/models/base/llm.ts
Normal file
28
services/master-agents-svc/src/lib/models/base/llm.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type z from 'zod';
|
||||
import type { Message } from '../../types.js';
|
||||
|
||||
export type Tool = {
|
||||
name: string;
|
||||
description: string;
|
||||
schema: z.ZodObject<Record<string, z.ZodTypeAny>>;
|
||||
};
|
||||
|
||||
export type ToolCall = {
|
||||
id: string;
|
||||
name: string;
|
||||
arguments: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type GenerateTextInput = {
|
||||
messages: Message[];
|
||||
tools?: Tool[];
|
||||
};
|
||||
|
||||
export type GenerateTextOutput = {
|
||||
content: string;
|
||||
toolCalls: ToolCall[];
|
||||
};
|
||||
|
||||
export abstract class BaseLLM {
|
||||
abstract generateText(input: GenerateTextInput): Promise<GenerateTextOutput>;
|
||||
}
|
||||
74
services/master-agents-svc/src/lib/models/ollama.ts
Normal file
74
services/master-agents-svc/src/lib/models/ollama.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { Ollama } from 'ollama';
|
||||
import z from 'zod';
|
||||
import crypto from 'node:crypto';
|
||||
import { BaseLLM } from './base/llm.js';
|
||||
import type { Message } from '../types.js';
|
||||
|
||||
type OllamaConfig = { baseURL: string; model: string };
|
||||
|
||||
export class OllamaLLM extends BaseLLM {
|
||||
private client: Ollama;
|
||||
private model: string;
|
||||
|
||||
constructor(config: OllamaConfig) {
|
||||
super();
|
||||
this.model = config.model;
|
||||
this.client = new Ollama({
|
||||
host: config.baseURL || 'http://localhost:11434',
|
||||
});
|
||||
}
|
||||
|
||||
private toOllama(msg: Message): { role: string; content: string; tool_calls?: { id: string; type: string; function: { name: string; arguments: unknown } }[] } {
|
||||
if (msg.role === 'tool') return { role: 'tool', content: msg.content } as { role: string; content: string };
|
||||
if (msg.role === 'assistant') {
|
||||
const m = msg as { content: string; tool_calls?: { id: string; name: string; arguments: Record<string, unknown> }[] };
|
||||
return {
|
||||
role: 'assistant',
|
||||
content: m.content,
|
||||
tool_calls: m.tool_calls?.map((tc) => ({
|
||||
id: tc.id,
|
||||
type: 'function',
|
||||
function: { name: tc.name, arguments: tc.arguments },
|
||||
})),
|
||||
};
|
||||
}
|
||||
return { role: msg.role, content: (msg as { content: string }).content };
|
||||
}
|
||||
|
||||
async generateText(input: { messages: Message[]; tools?: { name: string; description: string; schema: z.ZodObject<any> }[] }): Promise<{ content: string; toolCalls: { id: string; name: string; arguments: Record<string, unknown> }[] }> {
|
||||
const ollamaTools = input.tools?.map((t) => ({
|
||||
type: 'function' as const,
|
||||
function: {
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
parameters: (z.toJSONSchema(t.schema) as Record<string, unknown>).properties ?? {},
|
||||
},
|
||||
}));
|
||||
|
||||
const res = await this.client.chat({
|
||||
model: this.model,
|
||||
messages: input.messages.map((m) => this.toOllama(m)) as Parameters<typeof this.client.chat>[0]['messages'],
|
||||
tools: ollamaTools?.length ? ollamaTools : undefined,
|
||||
options: { temperature: 0.7, num_predict: 4096 },
|
||||
});
|
||||
|
||||
const toolCalls = (res.message.tool_calls ?? []).map((tc) => ({
|
||||
id: crypto.randomUUID(),
|
||||
name: tc.function.name,
|
||||
arguments: (() => {
|
||||
try {
|
||||
return typeof tc.function.arguments === 'string'
|
||||
? (JSON.parse(tc.function.arguments) as Record<string, unknown>)
|
||||
: (tc.function.arguments as Record<string, unknown> ?? {});
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
})(),
|
||||
}));
|
||||
|
||||
return {
|
||||
content: res.message.content ?? '',
|
||||
toolCalls,
|
||||
};
|
||||
}
|
||||
}
|
||||
90
services/master-agents-svc/src/lib/models/openai.ts
Normal file
90
services/master-agents-svc/src/lib/models/openai.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import OpenAI from 'openai';
|
||||
import z from 'zod';
|
||||
import { BaseLLM } from './base/llm.js';
|
||||
import type { Message, ToolMessage } from '../types.js';
|
||||
|
||||
type OpenAIConfig = {
|
||||
apiKey: string;
|
||||
model: string;
|
||||
baseURL?: string;
|
||||
};
|
||||
|
||||
export class OpenAILLM extends BaseLLM {
|
||||
private client: OpenAI;
|
||||
private model: string;
|
||||
|
||||
constructor(config: OpenAIConfig) {
|
||||
super();
|
||||
this.model = config.model;
|
||||
this.client = new OpenAI({
|
||||
apiKey: config.apiKey,
|
||||
baseURL: config.baseURL ?? 'https://api.openai.com/v1',
|
||||
});
|
||||
}
|
||||
|
||||
private toOpenAI(msg: Message): OpenAI.Chat.Completions.ChatCompletionMessageParam {
|
||||
if (msg.role === 'system') return { role: 'system', content: msg.content };
|
||||
if (msg.role === 'tool') {
|
||||
return {
|
||||
role: 'tool',
|
||||
tool_call_id: msg.id,
|
||||
content: msg.content,
|
||||
};
|
||||
}
|
||||
if (msg.role === 'assistant') {
|
||||
const m = msg as { content: string; tool_calls?: { id: string; name: string; arguments: Record<string, unknown> }[] };
|
||||
return {
|
||||
role: 'assistant',
|
||||
content: m.content,
|
||||
tool_calls: m.tool_calls?.map((tc) => ({
|
||||
id: tc.id,
|
||||
type: 'function' as const,
|
||||
function: { name: tc.name, arguments: JSON.stringify(tc.arguments) },
|
||||
})),
|
||||
};
|
||||
}
|
||||
return { role: 'user', content: msg.content };
|
||||
}
|
||||
|
||||
async generateText(input: { messages: Message[]; tools?: { name: string; description: string; schema: z.ZodObject<any> }[] }): Promise<{ content: string; toolCalls: { id: string; name: string; arguments: Record<string, unknown> }[] }> {
|
||||
const tools = input.tools?.map((t) => ({
|
||||
type: 'function' as const,
|
||||
function: {
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
parameters: z.toJSONSchema(t.schema),
|
||||
},
|
||||
}));
|
||||
|
||||
const res = await this.client.chat.completions.create({
|
||||
model: this.model,
|
||||
messages: input.messages.map((m) => this.toOpenAI(m)),
|
||||
tools: tools?.length ? tools : undefined,
|
||||
temperature: 0.7,
|
||||
max_tokens: 4096,
|
||||
});
|
||||
|
||||
const msg = res.choices?.[0]?.message;
|
||||
if (!msg) throw new Error('No response from OpenAI');
|
||||
|
||||
const toolCalls = (msg.tool_calls ?? []).map((tc) => {
|
||||
const fn = 'function' in tc ? (tc as { function?: { name?: string; arguments?: string } }).function : null;
|
||||
return {
|
||||
id: tc.id ?? crypto.randomUUID(),
|
||||
name: fn?.name ?? 'unknown',
|
||||
arguments: (() => {
|
||||
try {
|
||||
return JSON.parse(fn?.arguments ?? '{}') as Record<string, unknown>;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
})(),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
content: msg.content ?? '',
|
||||
toolCalls,
|
||||
};
|
||||
}
|
||||
}
|
||||
61
services/master-agents-svc/src/lib/models/registry.ts
Normal file
61
services/master-agents-svc/src/lib/models/registry.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { getProviderById } from '../config.js';
|
||||
import { OpenAILLM } from './openai.js';
|
||||
import { OllamaLLM } from './ollama.js';
|
||||
import type { BaseLLM } from './base/llm.js';
|
||||
|
||||
function getEnvProvider(): { type: string; config: Record<string, unknown> } | null {
|
||||
const p = (process.env.LLM_PROVIDER ?? '').toLowerCase();
|
||||
if (p === 'ollama') {
|
||||
const baseURL =
|
||||
process.env.OLLAMA_BASE_URL ??
|
||||
(process.env.DOCKER ? 'http://host.docker.internal:11434' : 'http://localhost:11434');
|
||||
return { type: 'ollama', config: { baseURL } };
|
||||
}
|
||||
if (p === 'openai' || process.env.OPENAI_API_KEY) {
|
||||
return {
|
||||
type: 'openai',
|
||||
config: {
|
||||
apiKey: process.env.OPENAI_API_KEY ?? '',
|
||||
baseURL: process.env.OPENAI_BASE_URL ?? 'https://api.openai.com/v1',
|
||||
},
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function loadChatModel(providerId: string, modelKey: string): Promise<BaseLLM> {
|
||||
let provider = getProviderById(providerId);
|
||||
if (!provider) {
|
||||
const env = getEnvProvider();
|
||||
if (env) {
|
||||
provider = {
|
||||
id: 'env',
|
||||
type: env.type,
|
||||
chatModels: [{ key: modelKey, name: modelKey }],
|
||||
config: env.config,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (!provider) throw new Error(`Provider not found: ${providerId}`);
|
||||
|
||||
const hasModel = provider.chatModels?.some((m) => m.key === modelKey);
|
||||
if (!hasModel && provider.id !== 'env') {
|
||||
throw new Error(`Model ${modelKey} not found in provider ${providerId}`);
|
||||
}
|
||||
|
||||
const cfg = provider.config as Record<string, unknown>;
|
||||
|
||||
if (provider.type === 'openai') {
|
||||
const apiKey = (cfg.apiKey as string) || process.env.OPENAI_API_KEY;
|
||||
const baseURL = (cfg.baseURL as string) || 'https://api.openai.com/v1';
|
||||
if (!apiKey) throw new Error('OpenAI API key not configured');
|
||||
return new OpenAILLM({ apiKey, model: modelKey, baseURL });
|
||||
}
|
||||
|
||||
if (provider.type === 'ollama') {
|
||||
const baseURL = (cfg.baseURL as string) || process.env.OLLAMA_BASE_URL || 'http://localhost:11434';
|
||||
return new OllamaLLM({ baseURL, model: modelKey });
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported provider type: ${provider.type}`);
|
||||
}
|
||||
22
services/master-agents-svc/src/lib/prompts/master.ts
Normal file
22
services/master-agents-svc/src/lib/prompts/master.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export const MASTER_SYSTEM_PROMPT = `You are a Master Agent that adapts to any task. You have access to tools and automatically decide which to use.
|
||||
|
||||
## Your role
|
||||
- Analyze the user's request and plan your approach
|
||||
- Use tools to gather information (web search, scrape URLs, get stock quotes, search images, calculator)
|
||||
- Synthesize results into a clear, helpful response
|
||||
- If a task requires multiple steps, use tools iteratively until you have enough information
|
||||
|
||||
## Available tools
|
||||
- web_search: Search the web. Use SEO-friendly keywords. Up to 3 queries per call.
|
||||
- scrape_url: Fetch and extract content from a specific URL. Use when user asks about a page.
|
||||
- calculator: Evaluate math expressions (arithmetic, percentages, sqrt, etc).
|
||||
- get_stock_quote: Get current stock price for a ticker (AAPL, TSLA, etc).
|
||||
- image_search: Search for relevant images by query.
|
||||
|
||||
## Rules
|
||||
- Always use tools when you need external information. Do not guess.
|
||||
- Prefer web_search for general knowledge; scrape_url only when user points to specific URLs.
|
||||
- Use get_stock_quote for stock prices; calculator for math.
|
||||
- After gathering info, provide a concise, accurate answer. Cite sources when relevant.
|
||||
- If a tool fails, try alternatives or inform the user.
|
||||
- Respond in the same language as the user's query unless asked otherwise.`;
|
||||
22
services/master-agents-svc/src/lib/tools/calculator.ts
Normal file
22
services/master-agents-svc/src/lib/tools/calculator.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import z from 'zod';
|
||||
import { evaluate } from 'mathjs';
|
||||
import type { ToolDef } from './types.js';
|
||||
|
||||
const schema = z.object({
|
||||
expression: z.string().describe('Math expression to evaluate, e.g. "2+3*4", "sqrt(16)"'),
|
||||
});
|
||||
|
||||
export const calculatorTool: ToolDef = {
|
||||
name: 'calculator',
|
||||
description: 'Evaluate a mathematical expression. Use for arithmetic, percentages, unit conversions.',
|
||||
schema,
|
||||
execute: async (params, _ctx) => {
|
||||
const { expression } = schema.parse(params);
|
||||
try {
|
||||
const result = evaluate(expression);
|
||||
return JSON.stringify({ expression, result: String(result) });
|
||||
} catch (err) {
|
||||
return JSON.stringify({ error: err instanceof Error ? err.message : 'Invalid expression' });
|
||||
}
|
||||
},
|
||||
};
|
||||
26
services/master-agents-svc/src/lib/tools/finance.ts
Normal file
26
services/master-agents-svc/src/lib/tools/finance.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import z from 'zod';
|
||||
import type { ToolDef } from './types.js';
|
||||
|
||||
const FINANCE_SVC = process.env.FINANCE_SVC_URL?.trim() ?? 'http://localhost:3003';
|
||||
|
||||
const quoteSchema = z.object({
|
||||
ticker: z.string().describe('Stock ticker symbol (e.g. AAPL, TSLA)'),
|
||||
});
|
||||
|
||||
export const quoteTool: ToolDef = {
|
||||
name: 'get_stock_quote',
|
||||
description: 'Get current stock price and quote data for a ticker symbol.',
|
||||
schema: quoteSchema,
|
||||
execute: async (params, _ctx) => {
|
||||
const { ticker } = quoteSchema.parse(params);
|
||||
const url = `${FINANCE_SVC.replace(/\/$/, '')}/api/v1/finance/quote/${encodeURIComponent(ticker)}`;
|
||||
try {
|
||||
const res = await fetch(url, { signal: AbortSignal.timeout(10000) });
|
||||
if (!res.ok) return JSON.stringify({ error: `HTTP ${res.status}` });
|
||||
const data = await res.json();
|
||||
return JSON.stringify(data, null, 2);
|
||||
} catch (err) {
|
||||
return JSON.stringify({ error: err instanceof Error ? err.message : 'Finance service unavailable' });
|
||||
}
|
||||
},
|
||||
};
|
||||
36
services/master-agents-svc/src/lib/tools/media.ts
Normal file
36
services/master-agents-svc/src/lib/tools/media.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import z from 'zod';
|
||||
import type { ToolDef } from './types.js';
|
||||
|
||||
const MEDIA_SVC = process.env.MEDIA_SVC_URL?.trim() ?? 'http://localhost:3016';
|
||||
|
||||
const imageSchema = z.object({
|
||||
query: z.string().describe('Image search query'),
|
||||
});
|
||||
|
||||
export const imageSearchTool: ToolDef = {
|
||||
name: 'image_search',
|
||||
description: 'Search for images by query. Use when user wants to see relevant images.',
|
||||
schema: imageSchema,
|
||||
execute: async (params, ctx) => {
|
||||
const parsed = imageSchema.parse(params);
|
||||
const chatModel = ctx?.chatModel ?? { providerId: 'env', key: process.env.LLM_CHAT_MODEL ?? 'gpt-4o-mini' };
|
||||
const url = `${MEDIA_SVC.replace(/\/$/, '')}/api/v1/media/images`;
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
query: parsed.query,
|
||||
chatHistory: ctx?.chatHistory ?? [],
|
||||
chatModel,
|
||||
}),
|
||||
signal: AbortSignal.timeout(15000),
|
||||
});
|
||||
if (!res.ok) return JSON.stringify({ error: `HTTP ${res.status}` });
|
||||
const data = (await res.json()) as { images?: { img_src: string; url: string; title: string }[] };
|
||||
return JSON.stringify({ images: data.images ?? [] }, null, 2);
|
||||
} catch (err) {
|
||||
return JSON.stringify({ error: err instanceof Error ? err.message : 'Media service unavailable' });
|
||||
}
|
||||
},
|
||||
};
|
||||
30
services/master-agents-svc/src/lib/tools/registry.ts
Normal file
30
services/master-agents-svc/src/lib/tools/registry.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { ToolDef } from './types.js';
|
||||
import { webSearchTool } from './search.js';
|
||||
import { scrapeUrlTool } from './scrape.js';
|
||||
import { calculatorTool } from './calculator.js';
|
||||
import { quoteTool } from './finance.js';
|
||||
import { imageSearchTool } from './media.js';
|
||||
|
||||
const ALL_TOOLS: ToolDef[] = [
|
||||
webSearchTool,
|
||||
scrapeUrlTool,
|
||||
calculatorTool,
|
||||
quoteTool,
|
||||
imageSearchTool,
|
||||
];
|
||||
|
||||
export function getTools(): ToolDef[] {
|
||||
return ALL_TOOLS;
|
||||
}
|
||||
|
||||
export function getTool(name: string): ToolDef | undefined {
|
||||
return ALL_TOOLS.find((t) => t.name === name);
|
||||
}
|
||||
|
||||
export function getToolSchemasForLLM(): { name: string; description: string; schema: ToolDef['schema'] }[] {
|
||||
return ALL_TOOLS.map((t) => ({
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
schema: t.schema,
|
||||
}));
|
||||
}
|
||||
31
services/master-agents-svc/src/lib/tools/scrape.ts
Normal file
31
services/master-agents-svc/src/lib/tools/scrape.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import z from 'zod';
|
||||
import type { ToolDef } from './types.js';
|
||||
|
||||
const schema = z.object({
|
||||
url: z.string().url().describe('URL to fetch and extract text from'),
|
||||
});
|
||||
|
||||
export const scrapeUrlTool: ToolDef = {
|
||||
name: 'scrape_url',
|
||||
description: 'Fetch and extract readable text from a URL. Use when user asks about specific page content.',
|
||||
schema,
|
||||
execute: async (params, _ctx) => {
|
||||
const { url } = schema.parse(params);
|
||||
const res = await fetch(url, {
|
||||
signal: AbortSignal.timeout(10000),
|
||||
headers: { 'User-Agent': 'GooSeek-MasterAgent/1.0' },
|
||||
});
|
||||
const html = await res.text();
|
||||
const title = html.match(/<title[^>]*>([^<]+)<\/title>/i)?.[1]?.trim() ?? url;
|
||||
const bodyMatch = html.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
|
||||
const body = bodyMatch ? bodyMatch[1] : html;
|
||||
const text = body
|
||||
.replace(/<script[\s\S]*?<\/script>/gi, '')
|
||||
.replace(/<style[\s\S]*?<\/style>/gi, '')
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
.slice(0, 8000);
|
||||
return JSON.stringify({ title, url, content: text }, null, 2);
|
||||
},
|
||||
};
|
||||
51
services/master-agents-svc/src/lib/tools/search.ts
Normal file
51
services/master-agents-svc/src/lib/tools/search.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import z from 'zod';
|
||||
import type { ToolDef } from './types.js';
|
||||
|
||||
const SEARCH_SVC = process.env.SEARCH_SVC_URL?.trim() ?? '';
|
||||
const SEARXNG = process.env.SEARXNG_URL?.trim() ?? 'https://searx.tiekoetter.com';
|
||||
|
||||
const schema = z.object({
|
||||
queries: z.array(z.string()).max(3).describe('Search queries (keywords, SEO-friendly)'),
|
||||
});
|
||||
|
||||
async function doSearch(q: string): Promise<{ results: { title: string; url: string; content?: string }[] }> {
|
||||
if (SEARCH_SVC) {
|
||||
const url = `${SEARCH_SVC.replace(/\/$/, '')}/api/v1/search?q=${encodeURIComponent(q)}`;
|
||||
const res = await fetch(url, { signal: AbortSignal.timeout(15000) });
|
||||
if (!res.ok) throw new Error(`Search HTTP ${res.status}`);
|
||||
const data = (await res.json()) as { results?: { title?: string; url?: string; content?: string }[] };
|
||||
return {
|
||||
results: (data.results ?? []).map((r) => ({
|
||||
title: r.title ?? '',
|
||||
url: r.url ?? '',
|
||||
content: r.content ?? '',
|
||||
})),
|
||||
};
|
||||
}
|
||||
const url = `${SEARXNG}/search?format=json&q=${encodeURIComponent(q)}`;
|
||||
const res = await fetch(url, { signal: AbortSignal.timeout(15000) });
|
||||
const data = (await res.json()) as { results?: { title?: string; url?: string; content?: string }[] };
|
||||
return {
|
||||
results: (data.results ?? []).map((r) => ({
|
||||
title: r.title ?? '',
|
||||
url: r.url ?? '',
|
||||
content: r.content ?? '',
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export const webSearchTool: ToolDef = {
|
||||
name: 'web_search',
|
||||
description: 'Search the web for information. Use SEO-friendly keywords. Up to 3 queries per call.',
|
||||
schema,
|
||||
execute: async (params, _ctx) => {
|
||||
const { queries } = schema.parse(params);
|
||||
const results = await Promise.all(queries.map(doSearch));
|
||||
const all = results.flatMap((r) => r.results).slice(0, 15);
|
||||
return JSON.stringify(
|
||||
all.map((r) => ({ title: r.title, url: r.url, snippet: (r.content ?? '').slice(0, 200) })),
|
||||
null,
|
||||
2,
|
||||
);
|
||||
},
|
||||
};
|
||||
13
services/master-agents-svc/src/lib/tools/types.ts
Normal file
13
services/master-agents-svc/src/lib/tools/types.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type z from 'zod';
|
||||
|
||||
export type AgentContext = {
|
||||
chatModel?: { providerId: string; key: string };
|
||||
chatHistory?: [string, string][];
|
||||
};
|
||||
|
||||
export type ToolDef = {
|
||||
name: string;
|
||||
description: string;
|
||||
schema: z.ZodObject<Record<string, z.ZodTypeAny>>;
|
||||
execute: (params: Record<string, unknown>, ctx?: AgentContext) => Promise<string>;
|
||||
};
|
||||
11
services/master-agents-svc/src/lib/types.ts
Normal file
11
services/master-agents-svc/src/lib/types.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export type UserMessage = { role: 'user'; content: string };
|
||||
export type AssistantMessage = {
|
||||
role: 'assistant';
|
||||
content: string;
|
||||
tool_calls?: { id: string; name: string; arguments: Record<string, unknown> }[];
|
||||
};
|
||||
export type SystemMessage = { role: 'system'; content: string };
|
||||
export type ToolMessage = { role: 'tool'; id: string; name: string; content: string };
|
||||
|
||||
export type Message = UserMessage | AssistantMessage | SystemMessage | ToolMessage;
|
||||
export type ChatTurnMessage = UserMessage | AssistantMessage;
|
||||
Reference in New Issue
Block a user