@radaros/core 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +35 -1
- package/dist/index.js +195 -12
- package/package.json +1 -1
- package/src/agent/agent.ts +4 -0
- package/src/index.ts +2 -0
- package/src/toolkits/duckduckgo.ts +256 -0
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as zod from 'zod';
|
|
1
2
|
import { z } from 'zod';
|
|
2
3
|
|
|
3
4
|
type MessageRole = "system" | "user" | "assistant" | "tool";
|
|
@@ -341,6 +342,7 @@ declare class Agent {
|
|
|
341
342
|
get modelId(): string;
|
|
342
343
|
get providerId(): string;
|
|
343
344
|
get hasStructuredOutput(): boolean;
|
|
345
|
+
get structuredOutputSchema(): zod.ZodSchema | undefined;
|
|
344
346
|
constructor(config: AgentConfig);
|
|
345
347
|
run(input: MessageContent, opts?: RunOpts): Promise<RunOutput>;
|
|
346
348
|
stream(input: MessageContent, opts?: RunOpts): AsyncGenerator<StreamChunk>;
|
|
@@ -1280,4 +1282,36 @@ declare class HackerNewsToolkit extends Toolkit {
|
|
|
1280
1282
|
getTools(): ToolDef[];
|
|
1281
1283
|
}
|
|
1282
1284
|
|
|
1283
|
-
|
|
1285
|
+
interface DuckDuckGoConfig {
|
|
1286
|
+
/** Enable web search (default true). */
|
|
1287
|
+
enableSearch?: boolean;
|
|
1288
|
+
/** Enable news search (default true). */
|
|
1289
|
+
enableNews?: boolean;
|
|
1290
|
+
/** Fixed max results per query (default 5). */
|
|
1291
|
+
maxResults?: number;
|
|
1292
|
+
}
|
|
1293
|
+
/**
|
|
1294
|
+
* DuckDuckGo Toolkit — search the web and news without any API key.
|
|
1295
|
+
*
|
|
1296
|
+
* Uses the DuckDuckGo HTML API (no key required).
|
|
1297
|
+
*
|
|
1298
|
+
* @example
|
|
1299
|
+
* ```ts
|
|
1300
|
+
* const ddg = new DuckDuckGoToolkit();
|
|
1301
|
+
* const agent = new Agent({ tools: [...ddg.getTools()] });
|
|
1302
|
+
* ```
|
|
1303
|
+
*/
|
|
1304
|
+
declare class DuckDuckGoToolkit extends Toolkit {
|
|
1305
|
+
readonly name = "duckduckgo";
|
|
1306
|
+
private config;
|
|
1307
|
+
constructor(config?: DuckDuckGoConfig);
|
|
1308
|
+
getTools(): ToolDef[];
|
|
1309
|
+
private buildSearchTool;
|
|
1310
|
+
private buildNewsTool;
|
|
1311
|
+
private search;
|
|
1312
|
+
private scrapeHtmlSearch;
|
|
1313
|
+
private decodeDdgUrl;
|
|
1314
|
+
private searchNews;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
export { type A2AAgentCard, type A2AArtifact, type A2ADataPart, type A2AFilePart, type A2AJsonRpcRequest, type A2AJsonRpcResponse, type A2AMessage, type A2APart, A2ARemoteAgent, type A2ARemoteAgentConfig, type A2ASendParams, type A2ASkill, type A2ATask, type A2ATaskQueryParams, type A2ATaskState, type A2ATextPart, Agent, type AgentConfig, type AgentEventMap, type AgentHooks, type AgentStep, AnthropicProvider, type Artifact, type AudioPart, BaseVectorStore, type ChatMessage, type ConditionStep, type ContentPart, type DuckDuckGoConfig, DuckDuckGoToolkit, type EmbeddingProvider, EventBus, type FilePart, type FunctionStep, type GmailConfig, GmailToolkit, GoogleEmbedding, type GoogleEmbeddingConfig, GoogleProvider, type GuardrailResult, type HackerNewsConfig, HackerNewsToolkit, type ImagePart, InMemoryStorage, InMemoryVectorStore, type InputGuardrail, KnowledgeBase, type KnowledgeBaseConfig, type KnowledgeBaseToolConfig, LLMLoop, type LogLevel, Logger, type LoggerConfig, MCPToolProvider, type MCPToolProviderConfig, Memory, type MemoryConfig, type MemoryEntry, type MessageContent, type MessageRole, type ModelConfig, type ModelProvider, ModelRegistry, type ModelResponse, MongoDBStorage, type MongoDBVectorConfig, MongoDBVectorStore, OllamaProvider, OpenAIEmbedding, type OpenAIEmbeddingConfig, OpenAIProvider, type OutputGuardrail, type ParallelStep, type PgVectorConfig, PgVectorStore, PostgresStorage, type QdrantConfig, QdrantVectorStore, RunContext, type RunOpts, type RunOutput, type Session, SessionManager, SqliteStorage, type StepDef, type StepResult, type StorageDriver, type StreamChunk, Team, type TeamConfig, TeamMode, type TextPart, type TokenUsage, type ToolCall, type ToolCallResult, type ToolDef, type ToolDefinition, ToolExecutor, type ToolResult, Toolkit, type VectorDocument, type VectorSearchOptions, type VectorSearchResult, type VectorStore, type VertexAIConfig, VertexAIProvider, type WebSearchConfig, WebSearchToolkit, type WhatsAppConfig, WhatsAppToolkit, Workflow, type WorkflowConfig, type WorkflowResult, anthropic, defineTool, getTextContent, google, isMultiModal, ollama, openai, registry, vertex };
|
package/dist/index.js
CHANGED
|
@@ -639,6 +639,9 @@ var Agent = class {
|
|
|
639
639
|
get hasStructuredOutput() {
|
|
640
640
|
return !!this.config.structuredOutput;
|
|
641
641
|
}
|
|
642
|
+
get structuredOutputSchema() {
|
|
643
|
+
return this.config.structuredOutput;
|
|
644
|
+
}
|
|
642
645
|
constructor(config) {
|
|
643
646
|
this.config = config;
|
|
644
647
|
this.name = config.name;
|
|
@@ -3787,14 +3790,14 @@ var MCPToolProvider = class {
|
|
|
3787
3790
|
await this.discoverTools();
|
|
3788
3791
|
}
|
|
3789
3792
|
async discoverTools() {
|
|
3790
|
-
const { z:
|
|
3793
|
+
const { z: z8 } = await import("zod");
|
|
3791
3794
|
const result = await this.client.listTools();
|
|
3792
3795
|
const mcpTools = result.tools ?? [];
|
|
3793
3796
|
this.tools = mcpTools.map((mcpTool) => {
|
|
3794
3797
|
const toolName = mcpTool.name;
|
|
3795
3798
|
const description = mcpTool.description ?? "";
|
|
3796
3799
|
const inputSchema = mcpTool.inputSchema ?? { type: "object", properties: {} };
|
|
3797
|
-
const parameters = this.jsonSchemaToZod(inputSchema,
|
|
3800
|
+
const parameters = this.jsonSchemaToZod(inputSchema, z8);
|
|
3798
3801
|
const execute = async (args, _ctx) => {
|
|
3799
3802
|
const callResult = await this.client.callTool({
|
|
3800
3803
|
name: toolName,
|
|
@@ -3822,9 +3825,9 @@ var MCPToolProvider = class {
|
|
|
3822
3825
|
};
|
|
3823
3826
|
});
|
|
3824
3827
|
}
|
|
3825
|
-
jsonSchemaToZod(schema,
|
|
3828
|
+
jsonSchemaToZod(schema, z8) {
|
|
3826
3829
|
if (!schema || !schema.properties) {
|
|
3827
|
-
return
|
|
3830
|
+
return z8.object({}).passthrough();
|
|
3828
3831
|
}
|
|
3829
3832
|
const shape = {};
|
|
3830
3833
|
const required = schema.required ?? [];
|
|
@@ -3832,24 +3835,24 @@ var MCPToolProvider = class {
|
|
|
3832
3835
|
let field;
|
|
3833
3836
|
switch (prop.type) {
|
|
3834
3837
|
case "string":
|
|
3835
|
-
field =
|
|
3836
|
-
if (prop.enum) field =
|
|
3838
|
+
field = z8.string();
|
|
3839
|
+
if (prop.enum) field = z8.enum(prop.enum);
|
|
3837
3840
|
break;
|
|
3838
3841
|
case "number":
|
|
3839
3842
|
case "integer":
|
|
3840
|
-
field =
|
|
3843
|
+
field = z8.number();
|
|
3841
3844
|
break;
|
|
3842
3845
|
case "boolean":
|
|
3843
|
-
field =
|
|
3846
|
+
field = z8.boolean();
|
|
3844
3847
|
break;
|
|
3845
3848
|
case "array":
|
|
3846
|
-
field =
|
|
3849
|
+
field = z8.array(z8.any());
|
|
3847
3850
|
break;
|
|
3848
3851
|
case "object":
|
|
3849
|
-
field =
|
|
3852
|
+
field = z8.record(z8.any());
|
|
3850
3853
|
break;
|
|
3851
3854
|
default:
|
|
3852
|
-
field =
|
|
3855
|
+
field = z8.any();
|
|
3853
3856
|
}
|
|
3854
3857
|
if (prop.description) {
|
|
3855
3858
|
field = field.describe(prop.description);
|
|
@@ -3859,7 +3862,7 @@ var MCPToolProvider = class {
|
|
|
3859
3862
|
}
|
|
3860
3863
|
shape[key] = field;
|
|
3861
3864
|
}
|
|
3862
|
-
return
|
|
3865
|
+
return z8.object(shape);
|
|
3863
3866
|
}
|
|
3864
3867
|
/**
|
|
3865
3868
|
* Returns tools from this MCP server as RadarOS ToolDef[].
|
|
@@ -4591,11 +4594,191 @@ var HackerNewsToolkit = class extends Toolkit {
|
|
|
4591
4594
|
return tools;
|
|
4592
4595
|
}
|
|
4593
4596
|
};
|
|
4597
|
+
|
|
4598
|
+
// src/toolkits/duckduckgo.ts
|
|
4599
|
+
import { z as z7 } from "zod";
|
|
4600
|
+
var DuckDuckGoToolkit = class extends Toolkit {
|
|
4601
|
+
name = "duckduckgo";
|
|
4602
|
+
config;
|
|
4603
|
+
constructor(config = {}) {
|
|
4604
|
+
super();
|
|
4605
|
+
this.config = {
|
|
4606
|
+
enableSearch: config.enableSearch ?? true,
|
|
4607
|
+
enableNews: config.enableNews ?? true,
|
|
4608
|
+
maxResults: config.maxResults ?? 5
|
|
4609
|
+
};
|
|
4610
|
+
}
|
|
4611
|
+
getTools() {
|
|
4612
|
+
const tools = [];
|
|
4613
|
+
if (this.config.enableSearch) {
|
|
4614
|
+
tools.push(this.buildSearchTool());
|
|
4615
|
+
}
|
|
4616
|
+
if (this.config.enableNews) {
|
|
4617
|
+
tools.push(this.buildNewsTool());
|
|
4618
|
+
}
|
|
4619
|
+
return tools;
|
|
4620
|
+
}
|
|
4621
|
+
buildSearchTool() {
|
|
4622
|
+
const self = this;
|
|
4623
|
+
return {
|
|
4624
|
+
name: "duckduckgo_search",
|
|
4625
|
+
description: "Search the web using DuckDuckGo. Returns titles, URLs, and snippets. No API key required.",
|
|
4626
|
+
parameters: z7.object({
|
|
4627
|
+
query: z7.string().describe("The search query"),
|
|
4628
|
+
maxResults: z7.number().optional().describe("Maximum number of results (default 5)")
|
|
4629
|
+
}),
|
|
4630
|
+
async execute(args, _ctx) {
|
|
4631
|
+
const query = args.query;
|
|
4632
|
+
const max = args.maxResults ?? self.config.maxResults ?? 5;
|
|
4633
|
+
return self.search(query, max);
|
|
4634
|
+
}
|
|
4635
|
+
};
|
|
4636
|
+
}
|
|
4637
|
+
buildNewsTool() {
|
|
4638
|
+
const self = this;
|
|
4639
|
+
return {
|
|
4640
|
+
name: "duckduckgo_news",
|
|
4641
|
+
description: "Get the latest news from DuckDuckGo. Returns headlines, sources, URLs, and dates.",
|
|
4642
|
+
parameters: z7.object({
|
|
4643
|
+
query: z7.string().describe("The news search query"),
|
|
4644
|
+
maxResults: z7.number().optional().describe("Maximum number of results (default 5)")
|
|
4645
|
+
}),
|
|
4646
|
+
async execute(args, _ctx) {
|
|
4647
|
+
const query = args.query;
|
|
4648
|
+
const max = args.maxResults ?? self.config.maxResults ?? 5;
|
|
4649
|
+
return self.searchNews(query, max);
|
|
4650
|
+
}
|
|
4651
|
+
};
|
|
4652
|
+
}
|
|
4653
|
+
async search(query, maxResults) {
|
|
4654
|
+
const url = `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json&no_redirect=1&no_html=1&skip_disambig=1`;
|
|
4655
|
+
const res = await fetch(url, {
|
|
4656
|
+
headers: { "User-Agent": "RadarOS/1.0" }
|
|
4657
|
+
});
|
|
4658
|
+
if (!res.ok) {
|
|
4659
|
+
throw new Error(`DuckDuckGo search failed: ${res.status}`);
|
|
4660
|
+
}
|
|
4661
|
+
const data = await res.json();
|
|
4662
|
+
const results = [];
|
|
4663
|
+
if (data.Abstract) {
|
|
4664
|
+
results.push(
|
|
4665
|
+
`Answer: ${data.Abstract}
|
|
4666
|
+
Source: ${data.AbstractSource}
|
|
4667
|
+
URL: ${data.AbstractURL}`
|
|
4668
|
+
);
|
|
4669
|
+
}
|
|
4670
|
+
const topics = data.RelatedTopics ?? [];
|
|
4671
|
+
for (const topic of topics.slice(0, maxResults)) {
|
|
4672
|
+
if (topic.Text && topic.FirstURL) {
|
|
4673
|
+
results.push(`${topic.Text}
|
|
4674
|
+
URL: ${topic.FirstURL}`);
|
|
4675
|
+
}
|
|
4676
|
+
if (topic.Topics) {
|
|
4677
|
+
for (const sub of topic.Topics.slice(0, 2)) {
|
|
4678
|
+
if (sub.Text && sub.FirstURL) {
|
|
4679
|
+
results.push(`${sub.Text}
|
|
4680
|
+
URL: ${sub.FirstURL}`);
|
|
4681
|
+
}
|
|
4682
|
+
}
|
|
4683
|
+
}
|
|
4684
|
+
}
|
|
4685
|
+
if (results.length === 0 && data.Redirect) {
|
|
4686
|
+
return `Redirect: ${data.Redirect}`;
|
|
4687
|
+
}
|
|
4688
|
+
if (results.length === 0) {
|
|
4689
|
+
const htmlResults = await this.scrapeHtmlSearch(query, maxResults);
|
|
4690
|
+
if (htmlResults) return htmlResults;
|
|
4691
|
+
return "No results found.";
|
|
4692
|
+
}
|
|
4693
|
+
return results.join("\n\n---\n\n");
|
|
4694
|
+
}
|
|
4695
|
+
async scrapeHtmlSearch(query, maxResults) {
|
|
4696
|
+
const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
|
|
4697
|
+
const res = await fetch(url, {
|
|
4698
|
+
headers: {
|
|
4699
|
+
"User-Agent": "Mozilla/5.0 (compatible; RadarOS/1.0; +https://radaros.dev)"
|
|
4700
|
+
}
|
|
4701
|
+
});
|
|
4702
|
+
if (!res.ok) return null;
|
|
4703
|
+
const html = await res.text();
|
|
4704
|
+
const results = [];
|
|
4705
|
+
const linkRegex = /<a[^>]*class="result__a"[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
4706
|
+
const snippetRegex = /<a[^>]*class="result__snippet"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
4707
|
+
const links = [];
|
|
4708
|
+
let match;
|
|
4709
|
+
while ((match = linkRegex.exec(html)) !== null) {
|
|
4710
|
+
const rawUrl = match[1];
|
|
4711
|
+
const title = match[2].replace(/<[^>]*>/g, "").trim();
|
|
4712
|
+
const decoded = this.decodeDdgUrl(rawUrl);
|
|
4713
|
+
if (title && decoded) {
|
|
4714
|
+
links.push({ url: decoded, title });
|
|
4715
|
+
}
|
|
4716
|
+
}
|
|
4717
|
+
const snippets = [];
|
|
4718
|
+
while ((match = snippetRegex.exec(html)) !== null) {
|
|
4719
|
+
snippets.push(match[1].replace(/<[^>]*>/g, "").trim());
|
|
4720
|
+
}
|
|
4721
|
+
for (let i = 0; i < Math.min(links.length, maxResults); i++) {
|
|
4722
|
+
results.push(
|
|
4723
|
+
`Title: ${links[i].title}
|
|
4724
|
+
URL: ${links[i].url}
|
|
4725
|
+
Snippet: ${snippets[i] ?? ""}`
|
|
4726
|
+
);
|
|
4727
|
+
}
|
|
4728
|
+
return results.length > 0 ? results.join("\n\n---\n\n") : null;
|
|
4729
|
+
}
|
|
4730
|
+
decodeDdgUrl(url) {
|
|
4731
|
+
if (url.startsWith("//duckduckgo.com/l/?uddg=")) {
|
|
4732
|
+
const match = url.match(/uddg=([^&]*)/);
|
|
4733
|
+
if (match) return decodeURIComponent(match[1]);
|
|
4734
|
+
}
|
|
4735
|
+
if (url.startsWith("http")) return url;
|
|
4736
|
+
return null;
|
|
4737
|
+
}
|
|
4738
|
+
async searchNews(query, maxResults) {
|
|
4739
|
+
const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query + " news")}&iar=news`;
|
|
4740
|
+
const res = await fetch(url, {
|
|
4741
|
+
headers: {
|
|
4742
|
+
"User-Agent": "Mozilla/5.0 (compatible; RadarOS/1.0; +https://radaros.dev)"
|
|
4743
|
+
}
|
|
4744
|
+
});
|
|
4745
|
+
if (!res.ok) {
|
|
4746
|
+
throw new Error(`DuckDuckGo news search failed: ${res.status}`);
|
|
4747
|
+
}
|
|
4748
|
+
const html = await res.text();
|
|
4749
|
+
const results = [];
|
|
4750
|
+
const linkRegex = /<a[^>]*class="result__a"[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
4751
|
+
const snippetRegex = /<a[^>]*class="result__snippet"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
4752
|
+
const links = [];
|
|
4753
|
+
let match;
|
|
4754
|
+
while ((match = linkRegex.exec(html)) !== null) {
|
|
4755
|
+
const rawUrl = match[1];
|
|
4756
|
+
const title = match[2].replace(/<[^>]*>/g, "").trim();
|
|
4757
|
+
const decoded = this.decodeDdgUrl(rawUrl);
|
|
4758
|
+
if (title && decoded) {
|
|
4759
|
+
links.push({ url: decoded, title });
|
|
4760
|
+
}
|
|
4761
|
+
}
|
|
4762
|
+
const snippets = [];
|
|
4763
|
+
while ((match = snippetRegex.exec(html)) !== null) {
|
|
4764
|
+
snippets.push(match[1].replace(/<[^>]*>/g, "").trim());
|
|
4765
|
+
}
|
|
4766
|
+
for (let i = 0; i < Math.min(links.length, maxResults); i++) {
|
|
4767
|
+
results.push(
|
|
4768
|
+
`Title: ${links[i].title}
|
|
4769
|
+
URL: ${links[i].url}
|
|
4770
|
+
Snippet: ${snippets[i] ?? ""}`
|
|
4771
|
+
);
|
|
4772
|
+
}
|
|
4773
|
+
return results.length > 0 ? results.join("\n\n---\n\n") : "No news results found.";
|
|
4774
|
+
}
|
|
4775
|
+
};
|
|
4594
4776
|
export {
|
|
4595
4777
|
A2ARemoteAgent,
|
|
4596
4778
|
Agent,
|
|
4597
4779
|
AnthropicProvider,
|
|
4598
4780
|
BaseVectorStore,
|
|
4781
|
+
DuckDuckGoToolkit,
|
|
4599
4782
|
EventBus,
|
|
4600
4783
|
GmailToolkit,
|
|
4601
4784
|
GoogleEmbedding,
|
package/package.json
CHANGED
package/src/agent/agent.ts
CHANGED
|
@@ -39,6 +39,10 @@ export class Agent {
|
|
|
39
39
|
return !!this.config.structuredOutput;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
get structuredOutputSchema(): import("zod").ZodSchema | undefined {
|
|
43
|
+
return this.config.structuredOutput;
|
|
44
|
+
}
|
|
45
|
+
|
|
42
46
|
constructor(config: AgentConfig) {
|
|
43
47
|
this.config = config;
|
|
44
48
|
this.name = config.name;
|
package/src/index.ts
CHANGED
|
@@ -146,3 +146,5 @@ export { WhatsAppToolkit } from "./toolkits/whatsapp.js";
|
|
|
146
146
|
export type { WhatsAppConfig } from "./toolkits/whatsapp.js";
|
|
147
147
|
export { HackerNewsToolkit } from "./toolkits/hackernews.js";
|
|
148
148
|
export type { HackerNewsConfig } from "./toolkits/hackernews.js";
|
|
149
|
+
export { DuckDuckGoToolkit } from "./toolkits/duckduckgo.js";
|
|
150
|
+
export type { DuckDuckGoConfig } from "./toolkits/duckduckgo.js";
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { ToolDef } from "../tools/types.js";
|
|
3
|
+
import type { RunContext } from "../agent/run-context.js";
|
|
4
|
+
import { Toolkit } from "./base.js";
|
|
5
|
+
|
|
6
|
+
export interface DuckDuckGoConfig {
|
|
7
|
+
/** Enable web search (default true). */
|
|
8
|
+
enableSearch?: boolean;
|
|
9
|
+
/** Enable news search (default true). */
|
|
10
|
+
enableNews?: boolean;
|
|
11
|
+
/** Fixed max results per query (default 5). */
|
|
12
|
+
maxResults?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* DuckDuckGo Toolkit — search the web and news without any API key.
|
|
17
|
+
*
|
|
18
|
+
* Uses the DuckDuckGo HTML API (no key required).
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* const ddg = new DuckDuckGoToolkit();
|
|
23
|
+
* const agent = new Agent({ tools: [...ddg.getTools()] });
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export class DuckDuckGoToolkit extends Toolkit {
|
|
27
|
+
readonly name = "duckduckgo";
|
|
28
|
+
private config: DuckDuckGoConfig;
|
|
29
|
+
|
|
30
|
+
constructor(config: DuckDuckGoConfig = {}) {
|
|
31
|
+
super();
|
|
32
|
+
this.config = {
|
|
33
|
+
enableSearch: config.enableSearch ?? true,
|
|
34
|
+
enableNews: config.enableNews ?? true,
|
|
35
|
+
maxResults: config.maxResults ?? 5,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
getTools(): ToolDef[] {
|
|
40
|
+
const tools: ToolDef[] = [];
|
|
41
|
+
|
|
42
|
+
if (this.config.enableSearch) {
|
|
43
|
+
tools.push(this.buildSearchTool());
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (this.config.enableNews) {
|
|
47
|
+
tools.push(this.buildNewsTool());
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return tools;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private buildSearchTool(): ToolDef {
|
|
54
|
+
const self = this;
|
|
55
|
+
return {
|
|
56
|
+
name: "duckduckgo_search",
|
|
57
|
+
description:
|
|
58
|
+
"Search the web using DuckDuckGo. Returns titles, URLs, and snippets. No API key required.",
|
|
59
|
+
parameters: z.object({
|
|
60
|
+
query: z.string().describe("The search query"),
|
|
61
|
+
maxResults: z
|
|
62
|
+
.number()
|
|
63
|
+
.optional()
|
|
64
|
+
.describe("Maximum number of results (default 5)"),
|
|
65
|
+
}),
|
|
66
|
+
async execute(
|
|
67
|
+
args: Record<string, unknown>,
|
|
68
|
+
_ctx: RunContext
|
|
69
|
+
): Promise<string> {
|
|
70
|
+
const query = args.query as string;
|
|
71
|
+
const max = (args.maxResults as number) ?? self.config.maxResults ?? 5;
|
|
72
|
+
return self.search(query, max);
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private buildNewsTool(): ToolDef {
|
|
78
|
+
const self = this;
|
|
79
|
+
return {
|
|
80
|
+
name: "duckduckgo_news",
|
|
81
|
+
description:
|
|
82
|
+
"Get the latest news from DuckDuckGo. Returns headlines, sources, URLs, and dates.",
|
|
83
|
+
parameters: z.object({
|
|
84
|
+
query: z.string().describe("The news search query"),
|
|
85
|
+
maxResults: z
|
|
86
|
+
.number()
|
|
87
|
+
.optional()
|
|
88
|
+
.describe("Maximum number of results (default 5)"),
|
|
89
|
+
}),
|
|
90
|
+
async execute(
|
|
91
|
+
args: Record<string, unknown>,
|
|
92
|
+
_ctx: RunContext
|
|
93
|
+
): Promise<string> {
|
|
94
|
+
const query = args.query as string;
|
|
95
|
+
const max = (args.maxResults as number) ?? self.config.maxResults ?? 5;
|
|
96
|
+
return self.searchNews(query, max);
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private async search(query: string, maxResults: number): Promise<string> {
|
|
102
|
+
const url = `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json&no_redirect=1&no_html=1&skip_disambig=1`;
|
|
103
|
+
|
|
104
|
+
const res = await fetch(url, {
|
|
105
|
+
headers: { "User-Agent": "RadarOS/1.0" },
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (!res.ok) {
|
|
109
|
+
throw new Error(`DuckDuckGo search failed: ${res.status}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const data = (await res.json()) as any;
|
|
113
|
+
const results: string[] = [];
|
|
114
|
+
|
|
115
|
+
if (data.Abstract) {
|
|
116
|
+
results.push(
|
|
117
|
+
`Answer: ${data.Abstract}\nSource: ${data.AbstractSource}\nURL: ${data.AbstractURL}`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const topics = data.RelatedTopics ?? [];
|
|
122
|
+
for (const topic of topics.slice(0, maxResults)) {
|
|
123
|
+
if (topic.Text && topic.FirstURL) {
|
|
124
|
+
results.push(`${topic.Text}\nURL: ${topic.FirstURL}`);
|
|
125
|
+
}
|
|
126
|
+
if (topic.Topics) {
|
|
127
|
+
for (const sub of topic.Topics.slice(0, 2)) {
|
|
128
|
+
if (sub.Text && sub.FirstURL) {
|
|
129
|
+
results.push(`${sub.Text}\nURL: ${sub.FirstURL}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (results.length === 0 && data.Redirect) {
|
|
136
|
+
return `Redirect: ${data.Redirect}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (results.length === 0) {
|
|
140
|
+
const htmlResults = await this.scrapeHtmlSearch(query, maxResults);
|
|
141
|
+
if (htmlResults) return htmlResults;
|
|
142
|
+
return "No results found.";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return results.join("\n\n---\n\n");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private async scrapeHtmlSearch(
|
|
149
|
+
query: string,
|
|
150
|
+
maxResults: number
|
|
151
|
+
): Promise<string | null> {
|
|
152
|
+
const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
|
|
153
|
+
|
|
154
|
+
const res = await fetch(url, {
|
|
155
|
+
headers: {
|
|
156
|
+
"User-Agent":
|
|
157
|
+
"Mozilla/5.0 (compatible; RadarOS/1.0; +https://radaros.dev)",
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (!res.ok) return null;
|
|
162
|
+
|
|
163
|
+
const html = await res.text();
|
|
164
|
+
|
|
165
|
+
const results: string[] = [];
|
|
166
|
+
const linkRegex =
|
|
167
|
+
/<a[^>]*class="result__a"[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
168
|
+
const snippetRegex =
|
|
169
|
+
/<a[^>]*class="result__snippet"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
170
|
+
|
|
171
|
+
const links: { url: string; title: string }[] = [];
|
|
172
|
+
let match;
|
|
173
|
+
while ((match = linkRegex.exec(html)) !== null) {
|
|
174
|
+
const rawUrl = match[1];
|
|
175
|
+
const title = match[2].replace(/<[^>]*>/g, "").trim();
|
|
176
|
+
const decoded = this.decodeDdgUrl(rawUrl);
|
|
177
|
+
if (title && decoded) {
|
|
178
|
+
links.push({ url: decoded, title });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const snippets: string[] = [];
|
|
183
|
+
while ((match = snippetRegex.exec(html)) !== null) {
|
|
184
|
+
snippets.push(match[1].replace(/<[^>]*>/g, "").trim());
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
for (let i = 0; i < Math.min(links.length, maxResults); i++) {
|
|
188
|
+
results.push(
|
|
189
|
+
`Title: ${links[i].title}\nURL: ${links[i].url}\nSnippet: ${snippets[i] ?? ""}`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return results.length > 0 ? results.join("\n\n---\n\n") : null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private decodeDdgUrl(url: string): string | null {
|
|
197
|
+
if (url.startsWith("//duckduckgo.com/l/?uddg=")) {
|
|
198
|
+
const match = url.match(/uddg=([^&]*)/);
|
|
199
|
+
if (match) return decodeURIComponent(match[1]);
|
|
200
|
+
}
|
|
201
|
+
if (url.startsWith("http")) return url;
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private async searchNews(
|
|
206
|
+
query: string,
|
|
207
|
+
maxResults: number
|
|
208
|
+
): Promise<string> {
|
|
209
|
+
const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query + " news")}&iar=news`;
|
|
210
|
+
|
|
211
|
+
const res = await fetch(url, {
|
|
212
|
+
headers: {
|
|
213
|
+
"User-Agent":
|
|
214
|
+
"Mozilla/5.0 (compatible; RadarOS/1.0; +https://radaros.dev)",
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
if (!res.ok) {
|
|
219
|
+
throw new Error(`DuckDuckGo news search failed: ${res.status}`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const html = await res.text();
|
|
223
|
+
const results: string[] = [];
|
|
224
|
+
|
|
225
|
+
const linkRegex =
|
|
226
|
+
/<a[^>]*class="result__a"[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
227
|
+
const snippetRegex =
|
|
228
|
+
/<a[^>]*class="result__snippet"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
229
|
+
|
|
230
|
+
const links: { url: string; title: string }[] = [];
|
|
231
|
+
let match;
|
|
232
|
+
while ((match = linkRegex.exec(html)) !== null) {
|
|
233
|
+
const rawUrl = match[1];
|
|
234
|
+
const title = match[2].replace(/<[^>]*>/g, "").trim();
|
|
235
|
+
const decoded = this.decodeDdgUrl(rawUrl);
|
|
236
|
+
if (title && decoded) {
|
|
237
|
+
links.push({ url: decoded, title });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const snippets: string[] = [];
|
|
242
|
+
while ((match = snippetRegex.exec(html)) !== null) {
|
|
243
|
+
snippets.push(match[1].replace(/<[^>]*>/g, "").trim());
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
for (let i = 0; i < Math.min(links.length, maxResults); i++) {
|
|
247
|
+
results.push(
|
|
248
|
+
`Title: ${links[i].title}\nURL: ${links[i].url}\nSnippet: ${snippets[i] ?? ""}`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return results.length > 0
|
|
253
|
+
? results.join("\n\n---\n\n")
|
|
254
|
+
: "No news results found.";
|
|
255
|
+
}
|
|
256
|
+
}
|