@stevederico/dotbot 0.16.0
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/CHANGELOG.md +136 -0
- package/README.md +380 -0
- package/bin/dotbot.js +461 -0
- package/core/agent.js +779 -0
- package/core/compaction.js +261 -0
- package/core/cron_handler.js +262 -0
- package/core/events.js +229 -0
- package/core/failover.js +193 -0
- package/core/gptoss_tool_parser.js +173 -0
- package/core/init.js +154 -0
- package/core/normalize.js +324 -0
- package/core/trigger_handler.js +148 -0
- package/docs/core.md +103 -0
- package/docs/protected-files.md +59 -0
- package/examples/sqlite-session-example.js +69 -0
- package/index.js +341 -0
- package/observer/index.js +164 -0
- package/package.json +42 -0
- package/storage/CronStore.js +145 -0
- package/storage/EventStore.js +71 -0
- package/storage/MemoryStore.js +175 -0
- package/storage/MongoAdapter.js +291 -0
- package/storage/MongoCronAdapter.js +347 -0
- package/storage/MongoTaskAdapter.js +242 -0
- package/storage/MongoTriggerAdapter.js +158 -0
- package/storage/SQLiteAdapter.js +382 -0
- package/storage/SQLiteCronAdapter.js +562 -0
- package/storage/SQLiteEventStore.js +300 -0
- package/storage/SQLiteMemoryAdapter.js +240 -0
- package/storage/SQLiteTaskAdapter.js +419 -0
- package/storage/SQLiteTriggerAdapter.js +262 -0
- package/storage/SessionStore.js +149 -0
- package/storage/TaskStore.js +100 -0
- package/storage/TriggerStore.js +90 -0
- package/storage/cron_constants.js +48 -0
- package/storage/index.js +21 -0
- package/tools/appgen.js +311 -0
- package/tools/browser.js +634 -0
- package/tools/code.js +101 -0
- package/tools/events.js +145 -0
- package/tools/files.js +201 -0
- package/tools/images.js +253 -0
- package/tools/index.js +97 -0
- package/tools/jobs.js +159 -0
- package/tools/memory.js +332 -0
- package/tools/messages.js +135 -0
- package/tools/notify.js +42 -0
- package/tools/tasks.js +404 -0
- package/tools/triggers.js +159 -0
- package/tools/weather.js +82 -0
- package/tools/web.js +283 -0
- package/utils/providers.js +136 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trigger Tools
|
|
3
|
+
*
|
|
4
|
+
* Event-driven agent responses. Triggers fire when specific events occur,
|
|
5
|
+
* injecting prompts into the agent conversation for context-aware assistance.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const triggerTools = [
|
|
9
|
+
{
|
|
10
|
+
name: "trigger_create",
|
|
11
|
+
description:
|
|
12
|
+
"Create an event-driven trigger that fires when a specific event occurs. " +
|
|
13
|
+
"Use cooldownMs to prevent spam (e.g., trigger max once per hour). " +
|
|
14
|
+
"Metadata can filter events (e.g., only fire for specific app names).",
|
|
15
|
+
parameters: {
|
|
16
|
+
type: "object",
|
|
17
|
+
properties: {
|
|
18
|
+
eventType: {
|
|
19
|
+
type: "string",
|
|
20
|
+
description: "Event type (user-defined string). Examples: app_opened, goal_completed, error_occurred, data_updated, task_completed, or any custom event",
|
|
21
|
+
},
|
|
22
|
+
prompt: {
|
|
23
|
+
type: "string",
|
|
24
|
+
description: "Prompt to inject into agent conversation when event fires",
|
|
25
|
+
},
|
|
26
|
+
cooldownMs: {
|
|
27
|
+
type: "number",
|
|
28
|
+
description: "Cooldown period in milliseconds (e.g., 3600000 for 1 hour). Default: 0 (no cooldown)",
|
|
29
|
+
},
|
|
30
|
+
metadata: {
|
|
31
|
+
type: "object",
|
|
32
|
+
description: "Optional metadata for filtering events (e.g., { appName: 'Mail' } for app_opened events)",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
required: ["eventType", "prompt"],
|
|
36
|
+
},
|
|
37
|
+
execute: async (input, signal, context) => {
|
|
38
|
+
if (!context?.triggerStore) return "Error: triggerStore not available";
|
|
39
|
+
try {
|
|
40
|
+
const trigger = await context.triggerStore.createTrigger({
|
|
41
|
+
userId: context.userID,
|
|
42
|
+
eventType: input.eventType,
|
|
43
|
+
prompt: input.prompt,
|
|
44
|
+
cooldownMs: input.cooldownMs || 0,
|
|
45
|
+
metadata: input.metadata || {},
|
|
46
|
+
enabled: true,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const cooldown = input.cooldownMs
|
|
50
|
+
? ` (cooldown: ${Math.round(input.cooldownMs / 1000)}s)`
|
|
51
|
+
: '';
|
|
52
|
+
return `Trigger created for event "${input.eventType}"${cooldown}`;
|
|
53
|
+
} catch (err) {
|
|
54
|
+
return `Error creating trigger: ${err.message}`;
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
{
|
|
60
|
+
name: "trigger_list",
|
|
61
|
+
description: "List all triggers, optionally filtered by enabled status or event type.",
|
|
62
|
+
parameters: {
|
|
63
|
+
type: "object",
|
|
64
|
+
properties: {
|
|
65
|
+
enabled: {
|
|
66
|
+
type: "boolean",
|
|
67
|
+
description: "Filter by enabled status (optional)",
|
|
68
|
+
},
|
|
69
|
+
eventType: {
|
|
70
|
+
type: "string",
|
|
71
|
+
description: "Filter by event type (optional)",
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
execute: async (input, signal, context) => {
|
|
76
|
+
if (!context?.triggerStore) return "Error: triggerStore not available";
|
|
77
|
+
try {
|
|
78
|
+
const filters = {};
|
|
79
|
+
if (input.enabled !== undefined) filters.enabled = input.enabled;
|
|
80
|
+
if (input.eventType) filters.eventType = input.eventType;
|
|
81
|
+
|
|
82
|
+
const triggers = await context.triggerStore.listTriggers(context.userID, filters);
|
|
83
|
+
|
|
84
|
+
if (triggers.length === 0) {
|
|
85
|
+
return input.enabled !== undefined || input.eventType
|
|
86
|
+
? "No triggers found matching filters."
|
|
87
|
+
: "No triggers yet. Create one with trigger_create.";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return triggers.map((t, i) => {
|
|
91
|
+
const status = t.enabled ? '✓' : '✗';
|
|
92
|
+
const cooldown = t.cooldownMs ? ` (cooldown: ${Math.round(t.cooldownMs / 1000)}s)` : '';
|
|
93
|
+
const fires = t.fireCount > 0 ? ` [fired ${t.fireCount}x]` : '';
|
|
94
|
+
const meta = Object.keys(t.metadata || {}).length > 0
|
|
95
|
+
? ` {${Object.entries(t.metadata).map(([k,v]) => `${k}:${v}`).join(', ')}}`
|
|
96
|
+
: '';
|
|
97
|
+
return `${status} ${t.eventType}${meta}${cooldown}${fires}\n → "${t.prompt.slice(0, 60)}${t.prompt.length > 60 ? '...' : ''}"`;
|
|
98
|
+
}).join('\n\n');
|
|
99
|
+
} catch (err) {
|
|
100
|
+
return `Error listing triggers: ${err.message}`;
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
{
|
|
106
|
+
name: "trigger_toggle",
|
|
107
|
+
description: "Enable or disable a trigger without deleting it.",
|
|
108
|
+
parameters: {
|
|
109
|
+
type: "object",
|
|
110
|
+
properties: {
|
|
111
|
+
trigger_id: { type: "string", description: "The trigger ID to toggle" },
|
|
112
|
+
enabled: { type: "boolean", description: "true to enable, false to disable" },
|
|
113
|
+
},
|
|
114
|
+
required: ["trigger_id", "enabled"],
|
|
115
|
+
},
|
|
116
|
+
execute: async (input, signal, context) => {
|
|
117
|
+
if (!context?.triggerStore) return "Error: triggerStore not available";
|
|
118
|
+
try {
|
|
119
|
+
const result = await context.triggerStore.toggleTrigger(
|
|
120
|
+
context.userID,
|
|
121
|
+
input.trigger_id,
|
|
122
|
+
input.enabled
|
|
123
|
+
);
|
|
124
|
+
if (result.modifiedCount > 0) {
|
|
125
|
+
return `Trigger ${input.trigger_id} ${input.enabled ? 'enabled' : 'disabled'}.`;
|
|
126
|
+
}
|
|
127
|
+
return "Trigger not found.";
|
|
128
|
+
} catch (err) {
|
|
129
|
+
return `Error toggling trigger: ${err.message}`;
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
{
|
|
135
|
+
name: "trigger_delete",
|
|
136
|
+
description: "Delete a trigger permanently.",
|
|
137
|
+
parameters: {
|
|
138
|
+
type: "object",
|
|
139
|
+
properties: {
|
|
140
|
+
trigger_id: { type: "string", description: "The trigger ID to delete" },
|
|
141
|
+
},
|
|
142
|
+
required: ["trigger_id"],
|
|
143
|
+
},
|
|
144
|
+
execute: async (input, signal, context) => {
|
|
145
|
+
if (!context?.triggerStore) return "Error: triggerStore not available";
|
|
146
|
+
try {
|
|
147
|
+
const result = await context.triggerStore.deleteTrigger(
|
|
148
|
+
context.userID,
|
|
149
|
+
input.trigger_id
|
|
150
|
+
);
|
|
151
|
+
return result.deletedCount > 0
|
|
152
|
+
? `Trigger ${input.trigger_id} deleted.`
|
|
153
|
+
: "Trigger not found.";
|
|
154
|
+
} catch (err) {
|
|
155
|
+
return `Error deleting trigger: ${err.message}`;
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
];
|
package/tools/weather.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Weather tool using Open-Meteo API (no API key required)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Convert WMO weather code to human-readable text.
|
|
7
|
+
* @param {number} code - WMO weather condition code
|
|
8
|
+
* @returns {string} Human-readable weather condition
|
|
9
|
+
*/
|
|
10
|
+
function weatherCodeToText(code) {
|
|
11
|
+
const codes = {
|
|
12
|
+
0: "Clear sky",
|
|
13
|
+
1: "Mainly clear", 2: "Partly cloudy", 3: "Overcast",
|
|
14
|
+
45: "Fog", 48: "Depositing rime fog",
|
|
15
|
+
51: "Light drizzle", 53: "Moderate drizzle", 55: "Dense drizzle",
|
|
16
|
+
56: "Light freezing drizzle", 57: "Dense freezing drizzle",
|
|
17
|
+
61: "Slight rain", 63: "Moderate rain", 65: "Heavy rain",
|
|
18
|
+
66: "Light freezing rain", 67: "Heavy freezing rain",
|
|
19
|
+
71: "Slight snow", 73: "Moderate snow", 75: "Heavy snow",
|
|
20
|
+
77: "Snow grains",
|
|
21
|
+
80: "Slight rain showers", 81: "Moderate rain showers", 82: "Violent rain showers",
|
|
22
|
+
85: "Slight snow showers", 86: "Heavy snow showers",
|
|
23
|
+
95: "Thunderstorm",
|
|
24
|
+
96: "Thunderstorm with slight hail", 99: "Thunderstorm with heavy hail"
|
|
25
|
+
};
|
|
26
|
+
return codes[code] || "Unknown";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const weatherTools = [
|
|
30
|
+
{
|
|
31
|
+
name: "weather_get",
|
|
32
|
+
description: "Get the current weather for a location.",
|
|
33
|
+
parameters: {
|
|
34
|
+
type: "object",
|
|
35
|
+
properties: {
|
|
36
|
+
location: { type: "string", description: "City name or location" },
|
|
37
|
+
},
|
|
38
|
+
required: ["location"],
|
|
39
|
+
},
|
|
40
|
+
execute: async (input, signal) => {
|
|
41
|
+
try {
|
|
42
|
+
// Geocode location
|
|
43
|
+
const geoRes = await fetch(
|
|
44
|
+
`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(input.location)}&count=1`,
|
|
45
|
+
{ signal }
|
|
46
|
+
);
|
|
47
|
+
if (!geoRes.ok) return `Geocoding failed: ${geoRes.status}`;
|
|
48
|
+
const geo = await geoRes.json();
|
|
49
|
+
if (!geo.results?.length) return `Location not found: ${input.location}`;
|
|
50
|
+
const { latitude, longitude, name, country } = geo.results[0];
|
|
51
|
+
|
|
52
|
+
// Fetch weather
|
|
53
|
+
const wxRes = await fetch(
|
|
54
|
+
`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code&temperature_unit=fahrenheit&wind_speed_unit=mph`,
|
|
55
|
+
{ signal }
|
|
56
|
+
);
|
|
57
|
+
if (!wxRes.ok) return `Weather fetch failed: ${wxRes.status}`;
|
|
58
|
+
const wx = await wxRes.json();
|
|
59
|
+
const c = wx.current;
|
|
60
|
+
return JSON.stringify({
|
|
61
|
+
_ui: {
|
|
62
|
+
component: "weather",
|
|
63
|
+
version: 1,
|
|
64
|
+
data: {
|
|
65
|
+
location: `${name}, ${country}`,
|
|
66
|
+
temperature: c.temperature_2m,
|
|
67
|
+
unit: "F",
|
|
68
|
+
humidity: c.relative_humidity_2m,
|
|
69
|
+
windSpeed: c.wind_speed_10m,
|
|
70
|
+
windUnit: "mph",
|
|
71
|
+
conditionCode: c.weather_code,
|
|
72
|
+
conditionText: weatherCodeToText(c.weather_code)
|
|
73
|
+
},
|
|
74
|
+
fallback: `Weather for ${name}: ${c.temperature_2m}°F, ${weatherCodeToText(c.weather_code)}`
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
} catch (err) {
|
|
78
|
+
return `Error getting weather: ${err.message}`;
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
];
|
package/tools/web.js
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web tools: search, fetch, grokipedia
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const webTools = [
|
|
6
|
+
// ── Web Search (Grok Responses API with web_search tool, fallback to DuckDuckGo) ──
|
|
7
|
+
{
|
|
8
|
+
name: "web_search",
|
|
9
|
+
description:
|
|
10
|
+
"Search the web for current information. Use ONLY ONCE per question to find the right URL — then use browser_navigate to read the actual page. Never call this tool multiple times for the same topic. For live or dynamic content (scores, dashboards), always prefer browser_navigate over repeated searches.",
|
|
11
|
+
parameters: {
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {
|
|
14
|
+
query: {
|
|
15
|
+
type: "string",
|
|
16
|
+
description: "The search query",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
required: ["query"],
|
|
20
|
+
},
|
|
21
|
+
execute: async (input, signal, context) => {
|
|
22
|
+
const apiKey = context?.providers?.xai?.apiKey;
|
|
23
|
+
|
|
24
|
+
// Primary: Use Grok Responses API with web_search tool
|
|
25
|
+
if (apiKey) {
|
|
26
|
+
try {
|
|
27
|
+
const res = await fetch("https://api.x.ai/v1/responses", {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: {
|
|
30
|
+
Authorization: `Bearer ${apiKey}`,
|
|
31
|
+
"Content-Type": "application/json",
|
|
32
|
+
},
|
|
33
|
+
body: JSON.stringify({
|
|
34
|
+
model: "grok-4-1-fast",
|
|
35
|
+
input: [
|
|
36
|
+
{
|
|
37
|
+
role: "system",
|
|
38
|
+
content: "You are a helpful search assistant. Provide concise, factual answers with sources. Format results as numbered points.",
|
|
39
|
+
},
|
|
40
|
+
{ role: "user", content: input.query },
|
|
41
|
+
],
|
|
42
|
+
tools: [{ type: "web_search" }],
|
|
43
|
+
}),
|
|
44
|
+
signal,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (res.ok) {
|
|
48
|
+
const data = await res.json();
|
|
49
|
+
const messageItem = (data.output || []).find(item => item.type === "message");
|
|
50
|
+
const contentItems = messageItem?.content || [];
|
|
51
|
+
|
|
52
|
+
const textContent = contentItems
|
|
53
|
+
.filter(c => c.type === "output_text")
|
|
54
|
+
.map(c => c.text)
|
|
55
|
+
.join("\n");
|
|
56
|
+
|
|
57
|
+
const citations = new Set();
|
|
58
|
+
for (const c of contentItems) {
|
|
59
|
+
if (c.annotations) {
|
|
60
|
+
for (const ann of c.annotations) {
|
|
61
|
+
if (ann.type === "url_citation" && ann.url) {
|
|
62
|
+
citations.add(ann.url);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let result = textContent;
|
|
69
|
+
if (citations.size > 0) {
|
|
70
|
+
result += "\n\nSources:\n" + [...citations].slice(0, 5).map((url, i) => `${i + 1}. ${url}`).join("\n");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (context?.databaseManager) {
|
|
74
|
+
try {
|
|
75
|
+
await context.databaseManager.logAgentActivity(
|
|
76
|
+
context.dbConfig.dbType, context.dbConfig.db, context.dbConfig.connectionString,
|
|
77
|
+
context.userID, { type: 'web_search', query: input.query, provider: 'grok', resultPreview: result.slice(0, 300) }
|
|
78
|
+
);
|
|
79
|
+
} catch (e) { /* best effort */ }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return result || "No results found.";
|
|
83
|
+
} else {
|
|
84
|
+
const errText = await res.text();
|
|
85
|
+
console.error("[web_search] Grok API error:", res.status, errText);
|
|
86
|
+
}
|
|
87
|
+
} catch (err) {
|
|
88
|
+
console.error("[web_search] Grok search failed:", err.message);
|
|
89
|
+
// Fall through to DuckDuckGo
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Fallback: DuckDuckGo Instant Answer API
|
|
94
|
+
const url = `https://api.duckduckgo.com/?q=${encodeURIComponent(input.query)}&format=json&no_html=1`;
|
|
95
|
+
const res = await fetch(url, {
|
|
96
|
+
headers: { "User-Agent": "DotBot/1.0" },
|
|
97
|
+
signal,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (!res.ok) {
|
|
101
|
+
return `Search failed: ${res.status} ${res.statusText}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const data = await res.json();
|
|
105
|
+
const parts = [];
|
|
106
|
+
|
|
107
|
+
if (data.Abstract) {
|
|
108
|
+
parts.push(`${data.Abstract}${data.AbstractSource ? ` (${data.AbstractSource})` : ""}${data.AbstractURL ? `\n ${data.AbstractURL}` : ""}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const topics = (data.RelatedTopics || []).filter((t) => t.Text && t.FirstURL);
|
|
112
|
+
for (let i = 0; i < Math.min(topics.length, 5); i++) {
|
|
113
|
+
const t = topics[i];
|
|
114
|
+
parts.push(`${parts.length + 1}. ${t.Text}\n ${t.FirstURL}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (parts.length === 0) {
|
|
118
|
+
return "No results found.";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const result = parts.join("\n\n");
|
|
122
|
+
|
|
123
|
+
if (context?.databaseManager) {
|
|
124
|
+
try {
|
|
125
|
+
await context.databaseManager.logAgentActivity(
|
|
126
|
+
context.dbConfig.dbType, context.dbConfig.db, context.dbConfig.connectionString,
|
|
127
|
+
context.userID, { type: 'web_search', query: input.query, provider: 'duckduckgo', resultPreview: parts.slice(0, 2).join('\n').slice(0, 300) }
|
|
128
|
+
);
|
|
129
|
+
} catch (e) { /* best effort */ }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return result;
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
// ── Grokipedia Search ──
|
|
137
|
+
{
|
|
138
|
+
name: "grokipedia_search",
|
|
139
|
+
description:
|
|
140
|
+
"Look up a topic on Grokipedia, a Wikipedia-like encyclopedia. Use this when the user asks to look something up on Grokipedia or wants an encyclopedic article.",
|
|
141
|
+
parameters: {
|
|
142
|
+
type: "object",
|
|
143
|
+
properties: {
|
|
144
|
+
query: {
|
|
145
|
+
type: "string",
|
|
146
|
+
description: "The topic to look up (e.g. 'JavaScript', 'Quantum computing')",
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
required: ["query"],
|
|
150
|
+
},
|
|
151
|
+
execute: async (input, signal, context) => {
|
|
152
|
+
try {
|
|
153
|
+
const url = `https://grokipedia.com/search?q=${encodeURIComponent(input.query)}`;
|
|
154
|
+
const res = await fetch(url, {
|
|
155
|
+
headers: { "User-Agent": "DotBot/1.0", Accept: "text/html" },
|
|
156
|
+
signal,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (res.status === 404) {
|
|
160
|
+
return `No Grokipedia article found for: ${input.query}`;
|
|
161
|
+
}
|
|
162
|
+
if (!res.ok) {
|
|
163
|
+
return `Grokipedia fetch failed: ${res.status} ${res.statusText}`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let text = await res.text();
|
|
167
|
+
text = text
|
|
168
|
+
.replace(/<script[\s\S]*?<\/script>/gi, "")
|
|
169
|
+
.replace(/<style[\s\S]*?<\/style>/gi, "")
|
|
170
|
+
.replace(/<[^>]+>/g, " ")
|
|
171
|
+
.replace(/\s+/g, " ")
|
|
172
|
+
.trim();
|
|
173
|
+
|
|
174
|
+
const maxChars = 8000;
|
|
175
|
+
if (text.length > maxChars) {
|
|
176
|
+
text = text.slice(0, maxChars) + `\n\n... [truncated, ${text.length} chars total]`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (context?.databaseManager) {
|
|
180
|
+
try {
|
|
181
|
+
await context.databaseManager.logAgentActivity(
|
|
182
|
+
context.dbConfig.dbType, context.dbConfig.db, context.dbConfig.connectionString,
|
|
183
|
+
context.userID, { type: 'grokipedia_search', query: input.query, url }
|
|
184
|
+
);
|
|
185
|
+
} catch (e) { /* best effort */ }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return text;
|
|
189
|
+
} catch (err) {
|
|
190
|
+
return `Error looking up Grokipedia: ${err.message}`;
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
// ── Web Fetch ──
|
|
196
|
+
{
|
|
197
|
+
name: "web_fetch",
|
|
198
|
+
description:
|
|
199
|
+
"Make an HTTP request and return the response. Supports GET, POST, PUT, PATCH, and DELETE. Use this to read web pages, call APIs, or send data to external services.",
|
|
200
|
+
parameters: {
|
|
201
|
+
type: "object",
|
|
202
|
+
properties: {
|
|
203
|
+
url: {
|
|
204
|
+
type: "string",
|
|
205
|
+
description: "The URL to fetch",
|
|
206
|
+
},
|
|
207
|
+
method: {
|
|
208
|
+
type: "string",
|
|
209
|
+
enum: ["GET", "POST", "PUT", "PATCH", "DELETE"],
|
|
210
|
+
description: "HTTP method (default GET)",
|
|
211
|
+
},
|
|
212
|
+
body: {
|
|
213
|
+
type: "string",
|
|
214
|
+
description: "Request body as a JSON string (for POST/PUT/PATCH)",
|
|
215
|
+
},
|
|
216
|
+
headers: {
|
|
217
|
+
type: "object",
|
|
218
|
+
description: "Additional request headers (e.g. { \"Authorization\": \"Bearer ...\" })",
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
required: ["url"],
|
|
222
|
+
},
|
|
223
|
+
execute: async (input, signal, context) => {
|
|
224
|
+
try {
|
|
225
|
+
const method = (input.method || "GET").toUpperCase();
|
|
226
|
+
const reqHeaders = {
|
|
227
|
+
"User-Agent": "DotBot/1.0",
|
|
228
|
+
Accept: "text/html,application/json,text/plain",
|
|
229
|
+
...(input.headers || {}),
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
if (input.body && !reqHeaders["Content-Type"] && !reqHeaders["content-type"]) {
|
|
233
|
+
reqHeaders["Content-Type"] = "application/json";
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const fetchOptions = { method, headers: reqHeaders, signal };
|
|
237
|
+
if (input.body && method !== "GET") {
|
|
238
|
+
fetchOptions.body = input.body;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const res = await fetch(input.url, fetchOptions);
|
|
242
|
+
|
|
243
|
+
if (!res.ok) {
|
|
244
|
+
return `Fetch failed: ${res.status} ${res.statusText}`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const contentType = res.headers.get("content-type") || "";
|
|
248
|
+
let text;
|
|
249
|
+
|
|
250
|
+
if (contentType.includes("application/json")) {
|
|
251
|
+
const json = await res.json();
|
|
252
|
+
text = JSON.stringify(json, null, 2);
|
|
253
|
+
} else {
|
|
254
|
+
text = await res.text();
|
|
255
|
+
text = text
|
|
256
|
+
.replace(/<script[\s\S]*?<\/script>/gi, "")
|
|
257
|
+
.replace(/<style[\s\S]*?<\/style>/gi, "")
|
|
258
|
+
.replace(/<[^>]+>/g, " ")
|
|
259
|
+
.replace(/\s+/g, " ")
|
|
260
|
+
.trim();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (context?.databaseManager) {
|
|
264
|
+
try {
|
|
265
|
+
await context.databaseManager.logAgentActivity(
|
|
266
|
+
context.dbConfig.dbType, context.dbConfig.db, context.dbConfig.connectionString,
|
|
267
|
+
context.userID, { type: 'web_fetch', url: input.url, status: res.status }
|
|
268
|
+
);
|
|
269
|
+
} catch (e) { /* best effort */ }
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const maxChars = 8000;
|
|
273
|
+
if (text.length > maxChars) {
|
|
274
|
+
return text.slice(0, maxChars) + `\n\n... [truncated, ${text.length} chars total]`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return text;
|
|
278
|
+
} catch (err) {
|
|
279
|
+
return `Error fetching URL: ${err.message}`;
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
];
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI provider registry for the agent library
|
|
3
|
+
*
|
|
4
|
+
* Provider configuration schema without environment variable coupling.
|
|
5
|
+
* API keys and base URLs should be injected at runtime via createAgent().
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Registry of supported AI providers.
|
|
10
|
+
*
|
|
11
|
+
* Each provider defines its API URL, default model, available models,
|
|
12
|
+
* header construction, request/response formatting, and endpoint path.
|
|
13
|
+
*/
|
|
14
|
+
export const AI_PROVIDERS = {
|
|
15
|
+
anthropic: {
|
|
16
|
+
id: 'anthropic',
|
|
17
|
+
name: 'Anthropic',
|
|
18
|
+
envKey: 'ANTHROPIC_API_KEY',
|
|
19
|
+
apiUrl: 'https://api.anthropic.com/v1',
|
|
20
|
+
defaultModel: 'claude-sonnet-4-5',
|
|
21
|
+
models: [
|
|
22
|
+
{ id: 'claude-sonnet-4-5', name: 'Claude Sonnet 4.5' },
|
|
23
|
+
{ id: 'claude-opus-4-1-20250805', name: 'Claude Opus 4.1' },
|
|
24
|
+
{ id: 'claude-sonnet-4-20250514', name: 'Claude Sonnet 4' },
|
|
25
|
+
{ id: 'claude-3-7-sonnet-20250219', name: 'Claude Sonnet 3.7' },
|
|
26
|
+
{ id: 'claude-3-5-haiku-20241022', name: 'Claude Haiku 3.5' }
|
|
27
|
+
],
|
|
28
|
+
headers: (apiKey) => ({
|
|
29
|
+
'x-api-key': apiKey,
|
|
30
|
+
'anthropic-version': '2025-04-14',
|
|
31
|
+
'Content-Type': 'application/json'
|
|
32
|
+
}),
|
|
33
|
+
endpoint: '/messages',
|
|
34
|
+
formatRequest: (messages, model) => ({
|
|
35
|
+
model,
|
|
36
|
+
max_tokens: 4096,
|
|
37
|
+
messages
|
|
38
|
+
}),
|
|
39
|
+
formatResponse: (data) => data.content?.[0]?.text
|
|
40
|
+
},
|
|
41
|
+
openai: {
|
|
42
|
+
id: 'openai',
|
|
43
|
+
name: 'OpenAI',
|
|
44
|
+
envKey: 'OPENAI_API_KEY',
|
|
45
|
+
apiUrl: 'https://api.openai.com/v1',
|
|
46
|
+
defaultModel: 'gpt-4o',
|
|
47
|
+
models: [
|
|
48
|
+
{ id: 'gpt-5', name: 'GPT-5' },
|
|
49
|
+
{ id: 'gpt-5-mini', name: 'GPT-5 Mini' },
|
|
50
|
+
{ id: 'gpt-5-nano', name: 'GPT-5 Nano' },
|
|
51
|
+
{ id: 'gpt-4.1', name: 'GPT-4.1' },
|
|
52
|
+
{ id: 'gpt-4.1-nano', name: 'GPT-4.1 Nano' },
|
|
53
|
+
{ id: 'gpt-4o', name: 'GPT-4o' }
|
|
54
|
+
],
|
|
55
|
+
headers: (apiKey) => ({
|
|
56
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
57
|
+
'Content-Type': 'application/json'
|
|
58
|
+
}),
|
|
59
|
+
endpoint: '/chat/completions',
|
|
60
|
+
formatRequest: (messages, model) => ({
|
|
61
|
+
model,
|
|
62
|
+
messages
|
|
63
|
+
}),
|
|
64
|
+
formatResponse: (data) => data.choices?.[0]?.message?.content
|
|
65
|
+
},
|
|
66
|
+
xai: {
|
|
67
|
+
id: 'xai',
|
|
68
|
+
name: 'xAI',
|
|
69
|
+
envKey: 'XAI_API_KEY',
|
|
70
|
+
apiUrl: 'https://api.x.ai/v1',
|
|
71
|
+
defaultModel: 'grok-4-1-fast-reasoning',
|
|
72
|
+
models: [
|
|
73
|
+
{ id: 'grok-4-1-fast-reasoning', name: 'Grok 4.1 Fast Reasoning' },
|
|
74
|
+
{ id: 'grok-4-1-fast-non-reasoning', name: 'Grok 4.1 Fast' },
|
|
75
|
+
{ id: 'grok-4-fast-reasoning', name: 'Grok 4 Fast Reasoning' },
|
|
76
|
+
{ id: 'grok-4-fast-non-reasoning', name: 'Grok 4 Fast' },
|
|
77
|
+
{ id: 'grok-4-0709', name: 'Grok 4' },
|
|
78
|
+
{ id: 'grok-code-fast-1', name: 'Grok Code Fast' },
|
|
79
|
+
{ id: 'grok-3', name: 'Grok 3' },
|
|
80
|
+
{ id: 'grok-3-mini', name: 'Grok 3 Mini' },
|
|
81
|
+
{ id: 'grok-2-vision-1212', name: 'Grok 2 Vision' }
|
|
82
|
+
],
|
|
83
|
+
headers: (apiKey) => ({
|
|
84
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
85
|
+
'Content-Type': 'application/json'
|
|
86
|
+
}),
|
|
87
|
+
endpoint: '/chat/completions',
|
|
88
|
+
formatRequest: (messages, model) => ({
|
|
89
|
+
model,
|
|
90
|
+
messages,
|
|
91
|
+
temperature: 0.7,
|
|
92
|
+
max_tokens: 4096
|
|
93
|
+
}),
|
|
94
|
+
formatResponse: (data) => data.choices?.[0]?.message?.content
|
|
95
|
+
},
|
|
96
|
+
cerebras: {
|
|
97
|
+
id: 'cerebras',
|
|
98
|
+
name: 'Cerebras',
|
|
99
|
+
envKey: 'CEREBRAS_API_KEY',
|
|
100
|
+
apiUrl: 'https://api.cerebras.ai/v1',
|
|
101
|
+
defaultModel: 'qwen-3-235b-a22b-instruct-2507',
|
|
102
|
+
models: [
|
|
103
|
+
{ id: 'llama3.1-8b', name: 'Llama 3.1 8B' },
|
|
104
|
+
{ id: 'qwen-3-235b-a22b-instruct-2507', name: 'Qwen 3 235B' },
|
|
105
|
+
{ id: 'gpt-oss-120b', name: 'GPT-OSS 120B' },
|
|
106
|
+
{ id: 'zai-glm-4.7', name: 'ZAI GLM 4.7' },
|
|
107
|
+
],
|
|
108
|
+
headers: (apiKey) => ({
|
|
109
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
110
|
+
'Content-Type': 'application/json'
|
|
111
|
+
}),
|
|
112
|
+
endpoint: '/chat/completions',
|
|
113
|
+
formatRequest: (messages, model) => ({
|
|
114
|
+
model,
|
|
115
|
+
messages
|
|
116
|
+
}),
|
|
117
|
+
formatResponse: (data) => data.choices?.[0]?.message?.content
|
|
118
|
+
},
|
|
119
|
+
ollama: {
|
|
120
|
+
id: 'ollama',
|
|
121
|
+
name: 'Ollama (Local)',
|
|
122
|
+
apiUrl: 'http://localhost:11434/v1',
|
|
123
|
+
defaultModel: 'gpt-oss:20b',
|
|
124
|
+
models: [],
|
|
125
|
+
local: true,
|
|
126
|
+
headers: () => ({
|
|
127
|
+
'Content-Type': 'application/json'
|
|
128
|
+
}),
|
|
129
|
+
endpoint: '/chat/completions',
|
|
130
|
+
formatRequest: (messages, model) => ({
|
|
131
|
+
model,
|
|
132
|
+
messages
|
|
133
|
+
}),
|
|
134
|
+
formatResponse: (data) => data.choices?.[0]?.message?.content
|
|
135
|
+
},
|
|
136
|
+
};
|