symposium 2.4.3 → 3.0.1
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/Agent.js +509 -219
- package/CLAUDE.md +101 -0
- package/Contexts/MCPResource.js +19 -0
- package/{GetContextTool.js → GetContextToolkit.js} +5 -5
- package/InputChannel.js +42 -0
- package/MCPServer.js +160 -0
- package/MIGRATION.md +369 -0
- package/Model.js +32 -25
- package/Models/AnthropicModel.js +66 -20
- package/Models/GrokModel.js +8 -8
- package/Models/GroqModel.js +61 -35
- package/Models/LegacyOpenAIModel.js +61 -35
- package/Models/OllamaModel.js +57 -31
- package/Models/OpenAIModel.js +74 -20
- package/README.md +458 -396
- package/Summarizer.js +5 -5
- package/Symposium.js +12 -12
- package/{Tool.js → Toolkit.js} +4 -4
- package/index.js +10 -2
- package/package.json +7 -3
- package/test/agent.test.js +698 -0
- package/test/helpers/mockSdk.js +52 -0
- package/test/mcp.test.js +216 -0
- package/test/models/anthropic.test.js +135 -0
- package/test/models/groq.test.js +71 -0
- package/test/models/legacyOpenai.test.js +87 -0
- package/test/models/ollama.test.js +90 -0
- package/test/models/openai.test.js +168 -0
- package/BufferedEventEmitter.js +0 -28
package/MIGRATION.md
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
# Migrating from Symposium 2.x to 3.0
|
|
2
|
+
|
|
3
|
+
Symposium 3.0 is a major release with breaking API changes. The core idea: replace the `EventEmitter`-based agent API with **async generators**, and let consumers push messages into a running agent via a **streaming input channel**.
|
|
4
|
+
|
|
5
|
+
This guide walks through the most common patterns side by side.
|
|
6
|
+
|
|
7
|
+
## TL;DR
|
|
8
|
+
|
|
9
|
+
| 2.x | 3.0 |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `agent.message()` returns an `EventEmitter` | `agent.message()` returns an `AsyncGenerator` for chat agents, `Promise<value>` for utility agents |
|
|
12
|
+
| `emitter.on('output', ...)` | `for await (const ev of agent.message(...))` |
|
|
13
|
+
| `emitter.on('error', ...)` | `try { for await ... } catch (err) { ... }` |
|
|
14
|
+
| `agent.confirmFunctions(thread, fns, completion, decision)` | `input.send({ type: 'auth', id, decision })` on the input channel |
|
|
15
|
+
| `agent.utility = { type, function, parameters }` | `agent.response_schema = <json-schema>` (works on chat agents too) |
|
|
16
|
+
| Fake "chunks" synthesized after the model finished | Real `chunk` events streamed as tokens arrive |
|
|
17
|
+
| No way to push messages mid-run | Pass an `AsyncIterable` (see `createInputChannel()`) |
|
|
18
|
+
|
|
19
|
+
## 1. Simple chat
|
|
20
|
+
|
|
21
|
+
**Before (2.x):**
|
|
22
|
+
|
|
23
|
+
```javascript
|
|
24
|
+
const emitter = await agent.message('Hello');
|
|
25
|
+
|
|
26
|
+
emitter.on('output', msg => {
|
|
27
|
+
if (msg.type === 'text')
|
|
28
|
+
process.stdout.write(msg.content);
|
|
29
|
+
});
|
|
30
|
+
emitter.on('error', err => console.error(err));
|
|
31
|
+
emitter.on('end', () => console.log('done'));
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**After (3.0):**
|
|
35
|
+
|
|
36
|
+
```javascript
|
|
37
|
+
try {
|
|
38
|
+
for await (const ev of agent.message('Hello')) {
|
|
39
|
+
if (ev.type === 'chunk')
|
|
40
|
+
process.stdout.write(ev.content);
|
|
41
|
+
}
|
|
42
|
+
console.log('done');
|
|
43
|
+
} catch (err) {
|
|
44
|
+
console.error(err);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Key changes:
|
|
49
|
+
|
|
50
|
+
- No `await` needed before iteration — `agent.message()` returns the generator synchronously.
|
|
51
|
+
- **Use `chunk` for incremental streaming.** It carries text deltas as they arrive from the provider (true streaming, finally). The old `output` event still exists, but it now carries the fully assembled content block for the assistant message — you'd use it if you only care about the final result.
|
|
52
|
+
- Errors throw out of the generator. The `error` event is gone — wrap the loop in `try/catch`.
|
|
53
|
+
- The `end` event still exists but mainly for side-channel cleanup; the loop simply finishes.
|
|
54
|
+
|
|
55
|
+
## 2. Tools
|
|
56
|
+
|
|
57
|
+
**Before (2.x):**
|
|
58
|
+
|
|
59
|
+
```javascript
|
|
60
|
+
const emitter = await agent.message('What is the weather in Paris?');
|
|
61
|
+
|
|
62
|
+
emitter.on('tool', tool => {
|
|
63
|
+
console.log(`> calling ${tool.name}`);
|
|
64
|
+
});
|
|
65
|
+
emitter.on('tool_response', resp => {
|
|
66
|
+
if (resp.success)
|
|
67
|
+
console.log(`> ${resp.name} OK`);
|
|
68
|
+
else
|
|
69
|
+
console.log(`> ${resp.name} FAILED: ${resp.error}`);
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**After (3.0):**
|
|
74
|
+
|
|
75
|
+
```javascript
|
|
76
|
+
for await (const ev of agent.message('What is the weather in Paris?')) {
|
|
77
|
+
switch (ev.type) {
|
|
78
|
+
case 'tool':
|
|
79
|
+
console.log(`> calling ${ev.name}`);
|
|
80
|
+
break;
|
|
81
|
+
case 'tool_response':
|
|
82
|
+
if (ev.success)
|
|
83
|
+
console.log(`> ${ev.name} OK`);
|
|
84
|
+
else
|
|
85
|
+
console.log(`> ${ev.name} FAILED: ${ev.error}`);
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Same event shapes, different delivery mechanism. Note that tools within a single LLM turn now execute **sequentially** in the order the model requested them, so `tool` / `tool_response` events arrive in deterministic order.
|
|
92
|
+
|
|
93
|
+
## 3. Tool authorization
|
|
94
|
+
|
|
95
|
+
This is the biggest behavioral change. The old `confirmFunctions` callback is gone.
|
|
96
|
+
|
|
97
|
+
**Before (2.x):**
|
|
98
|
+
|
|
99
|
+
```javascript
|
|
100
|
+
const emitter = await agent.message('Run that risky thing');
|
|
101
|
+
|
|
102
|
+
emitter.on('tools_auth', async ({ thread, functions, completion }) => {
|
|
103
|
+
const decision = await askUser(functions); // 'approve' | 'approve_always' | 'reject'
|
|
104
|
+
await agent.confirmFunctions(thread, functions, completion, decision);
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**After (3.0):**
|
|
109
|
+
|
|
110
|
+
```javascript
|
|
111
|
+
import { createInputChannel } from 'symposium';
|
|
112
|
+
|
|
113
|
+
const input = createInputChannel();
|
|
114
|
+
input.send('Run that risky thing');
|
|
115
|
+
|
|
116
|
+
for await (const ev of agent.message(input)) {
|
|
117
|
+
if (ev.type === 'tools_auth') {
|
|
118
|
+
const decision = await askUser(ev.tools); // 'approve' | 'approve_always' | 'reject'
|
|
119
|
+
input.send({ type: 'auth', id: ev.id, decision });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Key changes:
|
|
125
|
+
|
|
126
|
+
- You now need an **input channel** to deliver the auth decision (there's no callback to call).
|
|
127
|
+
- The `tools_auth` event carries an `id` (UUID) — echo it back in the `auth` control message so the agent knows which pending batch you're answering.
|
|
128
|
+
- If you call `agent.message()` with a plain string and a tool requires auth, the agent **auto-rejects** the call (no channel = no way to respond).
|
|
129
|
+
- If the channel closes while a `tools_auth` is pending, it's treated as a reject and the run cancels.
|
|
130
|
+
|
|
131
|
+
## 4. Structured output
|
|
132
|
+
|
|
133
|
+
The old `utility = { type, function, parameters }` shape was removed. Use `response_schema` (a raw JSON schema) instead. `response_schema` is independent of the agent type — it works on chat agents too.
|
|
134
|
+
|
|
135
|
+
**Before (2.x):**
|
|
136
|
+
|
|
137
|
+
```javascript
|
|
138
|
+
class ExtractorAgent extends Agent {
|
|
139
|
+
type = 'utility';
|
|
140
|
+
utility = {
|
|
141
|
+
type: 'json',
|
|
142
|
+
function: {
|
|
143
|
+
name: 'extract',
|
|
144
|
+
parameters: {
|
|
145
|
+
type: 'object',
|
|
146
|
+
properties: {
|
|
147
|
+
name: { type: 'string' },
|
|
148
|
+
email: { type: 'string' },
|
|
149
|
+
},
|
|
150
|
+
required: ['name', 'email'],
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const result = await extractor.message('My name is John, john@example.com');
|
|
157
|
+
// result was already the parsed value — that part hasn't changed.
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**After (3.0):**
|
|
161
|
+
|
|
162
|
+
```javascript
|
|
163
|
+
class ExtractorAgent extends Agent {
|
|
164
|
+
type = 'utility';
|
|
165
|
+
response_schema = {
|
|
166
|
+
type: 'object',
|
|
167
|
+
properties: {
|
|
168
|
+
name: { type: 'string' },
|
|
169
|
+
email: { type: 'string' },
|
|
170
|
+
},
|
|
171
|
+
required: ['name', 'email'],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const result = await extractor.message('My name is John, john@example.com');
|
|
176
|
+
// { name: 'John', email: 'john@example.com' }
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
For a **chat agent with a structured final answer**, the parsed value arrives as a `result` event right before `end`:
|
|
180
|
+
|
|
181
|
+
```javascript
|
|
182
|
+
agent.response_schema = { type: 'object', properties: { city: { type: 'string' } }, required: ['city'] };
|
|
183
|
+
|
|
184
|
+
for await (const ev of agent.message('Look up the weather and answer in JSON')) {
|
|
185
|
+
if (ev.type === 'result')
|
|
186
|
+
console.log(ev.value); // { city: '...' }
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## 5. Streaming user input
|
|
191
|
+
|
|
192
|
+
New in 3.0 — there was no 2.x equivalent.
|
|
193
|
+
|
|
194
|
+
```javascript
|
|
195
|
+
import { createInputChannel } from 'symposium';
|
|
196
|
+
|
|
197
|
+
const input = createInputChannel();
|
|
198
|
+
input.send('Plan a trip to Rome');
|
|
199
|
+
|
|
200
|
+
const events = agent.message(input);
|
|
201
|
+
|
|
202
|
+
// Concurrently, from anywhere else:
|
|
203
|
+
setTimeout(() => input.send('Actually, make it Florence instead'), 2000);
|
|
204
|
+
|
|
205
|
+
for await (const ev of events) {
|
|
206
|
+
if (ev.type === 'chunk')
|
|
207
|
+
process.stdout.write(ev.content);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// End the run when you're done sending:
|
|
211
|
+
input.close();
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Behavior:
|
|
215
|
+
|
|
216
|
+
- The agent starts the first model turn once content has arrived (or once a `{type:'submit'}` control message lands).
|
|
217
|
+
- New items pushed mid-turn are queued and inserted as a `user` message at the next inter-turn boundary (after the current tool batch finishes — there is no mid-turn cancellation of the model call).
|
|
218
|
+
- The run continues across multiple turns until you `input.close()` or send `{type:'cancel'}`.
|
|
219
|
+
|
|
220
|
+
## 6. Retries
|
|
221
|
+
|
|
222
|
+
Agents always retried on transport errors. In 3.0 the retry is now visible to the consumer **when it matters**:
|
|
223
|
+
|
|
224
|
+
- If no `chunk` has been streamed yet for the current turn → silent retry (most cases: connection error before any tokens).
|
|
225
|
+
- If at least one `chunk` has streamed → yield `{type:'retry', attempt, reason}` so the consumer can clear partial output or show a spinner.
|
|
226
|
+
|
|
227
|
+
```javascript
|
|
228
|
+
for await (const ev of agent.message('Hello')) {
|
|
229
|
+
if (ev.type === 'retry')
|
|
230
|
+
console.warn(`retrying (${ev.attempt}): ${ev.reason}`);
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## 7. Lifecycle hooks
|
|
235
|
+
|
|
236
|
+
If you've subclassed `Agent` and overridden `beforeExecute` / `afterExecute` / `afterHandle`, the `emitter` parameter was removed:
|
|
237
|
+
|
|
238
|
+
```javascript
|
|
239
|
+
// Before:
|
|
240
|
+
async afterHandle(thread, completion, emitter) { ... }
|
|
241
|
+
|
|
242
|
+
// After:
|
|
243
|
+
async afterHandle(thread, completion, value) { ... }
|
|
244
|
+
// `value` is the parsed result when `response_schema` is set; undefined otherwise.
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
If your hook used to emit events on the emitter, that's no longer possible directly. Hooks now run inside the generator pipeline — if you need to surface information, return it from the hook or stash it on the thread state and let the consumer read it from the events.
|
|
248
|
+
|
|
249
|
+
## 8. `BufferedEventEmitter`
|
|
250
|
+
|
|
251
|
+
Deleted. The class only existed to paper over the listener-attach race created by `agent.message()` returning an emitter. With async generators, the issue doesn't exist.
|
|
252
|
+
|
|
253
|
+
## 9. Things that did NOT change
|
|
254
|
+
|
|
255
|
+
- Agent / Thread / Toolkit / Message / Context class shapes.
|
|
256
|
+
- Model registration (drop a file in `Models/`, `Symposium.init()` picks it up).
|
|
257
|
+
- Storage adapter interface (`init` / `get` / `set`).
|
|
258
|
+
- `Symposium.prompt()` — still a one-shot value-returning helper.
|
|
259
|
+
- `Symposium.transcribe()` / `Symposium.embed()`.
|
|
260
|
+
- Real-time session API (`agent.createRealtimeSession()`).
|
|
261
|
+
- Italian-language fallback prompt in `Model.promptFromTools()` and the realtime session preamble.
|
|
262
|
+
|
|
263
|
+
## 10. Notes & gotchas
|
|
264
|
+
|
|
265
|
+
- **Backpressure.** Async generators are pull-driven: a slow consumer pauses the model upstream. This is generally an improvement over fire-and-forget emitter events, but it's a behavioral change to be aware of.
|
|
266
|
+
- **Multi-consumer.** Async generators are single-consumer. If you need to fan out an agent run to two listeners, tee it manually.
|
|
267
|
+
- **`message()` is not `async`.** It returns the generator synchronously for chat agents (no `await`). Utility agents return a Promise — same as before in spirit, but it now resolves to the value directly without a separate event loop.
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
# Additional breaking changes in 3.0
|
|
272
|
+
|
|
273
|
+
3.0 ships a vocabulary cleanup to align with industry terminology (OpenAI/Anthropic/LangChain all use **tool** and **toolkit**, not **function**). It also adds first-class **MCP** support — see the `addMCPServer()` section in `README.md` / `CLAUDE.md`. The renames below are purely cosmetic but pervasive; runtime semantics are unchanged.
|
|
274
|
+
|
|
275
|
+
## A. `Tool` class → `Toolkit`
|
|
276
|
+
|
|
277
|
+
The base class for "a thing that publishes one or more LLM-callable tools" is now `Toolkit`. The word **tool** is reserved for the individual callable unit (which is what the LLM actually sees).
|
|
278
|
+
|
|
279
|
+
| 2.x | 3.x |
|
|
280
|
+
|-----------------------------------------------------|--------------------------------------------------------|
|
|
281
|
+
| `Tool.js` / `GetContextTool.js` | `Toolkit.js` / `GetContextToolkit.js` |
|
|
282
|
+
| `import { Tool } from 'symposium'` | `import { Toolkit } from 'symposium'` |
|
|
283
|
+
| `class Weather extends Tool` | `class Weather extends Toolkit` |
|
|
284
|
+
| `agent.addTool(t)` | `agent.addToolkit(t)` |
|
|
285
|
+
| `agent.tools` — `Map<toolkitName, Toolkit>` | `agent.toolkits` — `Map<toolkitName, Toolkit>` |
|
|
286
|
+
| *(internal: `agent.functions` / `agent.toolIndex`)* | `agent.tools` — `Map<toolName, {toolkit, definition}>` |
|
|
287
|
+
|
|
288
|
+
The two maps swapped roles on purpose: `agent.tools` is now the flat lookup of LLM-callables (matching how every provider's API talks about "tools"), and `agent.toolkits` is the registry of `Toolkit` instances. Lookup entries are now `{toolkit, definition}` (was `{tool, function}`).
|
|
289
|
+
|
|
290
|
+
## B. `function` → `tool` everywhere
|
|
291
|
+
|
|
292
|
+
The framework no longer uses the word **function** for LLM-callable units. Method names, parameter names, message content types, option keys, and event payload keys all changed.
|
|
293
|
+
|
|
294
|
+
### Method renames
|
|
295
|
+
|
|
296
|
+
| 2.x | 3.x |
|
|
297
|
+
|--------------------------------------------------|--------------------------------------------|
|
|
298
|
+
| `Toolkit.getFunctions()` | `Toolkit.getTools()` |
|
|
299
|
+
| `Toolkit.callFunction(thread, name, payload)` | `Toolkit.callTool(thread, name, payload)` |
|
|
300
|
+
| `Agent.getFunctions()` | `Agent.getTools()` |
|
|
301
|
+
| `Agent.callFunction()` / `Agent.callFunctions()` | `Agent.callTool()` / `Agent.callTools()` |
|
|
302
|
+
| `Agent.parseFunctions()` | `Agent.parseTools()` |
|
|
303
|
+
| `Model.promptFromFunctions()` | `Model.promptFromTools()` |
|
|
304
|
+
| `Symposium.extractFunctionsFromResponse()` | `Symposium.extractToolCallsFromResponse()` |
|
|
305
|
+
|
|
306
|
+
### Provider signature
|
|
307
|
+
|
|
308
|
+
```javascript
|
|
309
|
+
// before
|
|
310
|
+
const parsed = this.parseOptions(options, functions);
|
|
311
|
+
|
|
312
|
+
// after
|
|
313
|
+
const parsed = this.parseOptions(options, tools);
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
`parseOptions()` now returns `{options, tools}` (was `{options, functions}`).
|
|
317
|
+
|
|
318
|
+
### Options
|
|
319
|
+
|
|
320
|
+
| 2.x | 3.0 |
|
|
321
|
+
|----------------------------------|------------------------------|
|
|
322
|
+
| `options.functions: [...]` | `options.tools: [...]` |
|
|
323
|
+
| `options.force_function: 'name'` | `options.force_tool: 'name'` |
|
|
324
|
+
|
|
325
|
+
### Internal Message content block types
|
|
326
|
+
|
|
327
|
+
If you build `Message` objects by hand or inspect a thread's history, the content-block tags changed:
|
|
328
|
+
|
|
329
|
+
| 2.x | 3.0 |
|
|
330
|
+
|--------------------------------------------------------------|--------------------------------------------------------------|
|
|
331
|
+
| `{type: 'function', content: [{id, name, arguments}, ...]}` | `{type: 'tool_call', content: [{id, name, arguments}, ...]}` |
|
|
332
|
+
| `{type: 'function_response', content: {name, id, response}}` | `{type: 'tool_result', content: {name, id, response}}` |
|
|
333
|
+
|
|
334
|
+
Provider wire formats (e.g. OpenAI's `{type: 'function', function: {...}}` tool-definition shape and `tool_calls[].type = 'function'`) are unchanged — those are the providers' contract, not Symposium's.
|
|
335
|
+
|
|
336
|
+
### Event payload key
|
|
337
|
+
|
|
338
|
+
```javascript
|
|
339
|
+
// before
|
|
340
|
+
if (ev.type === 'tools_auth') {
|
|
341
|
+
const decision = await askUser(ev.functions);
|
|
342
|
+
input.send({ type: 'auth', id: ev.id, decision });
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// after
|
|
346
|
+
if (ev.type === 'tools_auth') {
|
|
347
|
+
const decision = await askUser(ev.tools);
|
|
348
|
+
input.send({ type: 'auth', id: ev.id, decision });
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
## C. Mechanical migration
|
|
353
|
+
|
|
354
|
+
For most consumers, a project-wide find-and-replace covers it:
|
|
355
|
+
|
|
356
|
+
```
|
|
357
|
+
Tool → Toolkit (class references, imports — careful with the literal string "tool")
|
|
358
|
+
extends Tool → extends Toolkit
|
|
359
|
+
GetContextTool → GetContextToolkit
|
|
360
|
+
addTool → addToolkit
|
|
361
|
+
getFunctions → getTools
|
|
362
|
+
callFunction → callTool
|
|
363
|
+
parseFunctions → parseTools
|
|
364
|
+
promptFromFunctions → promptFromTools
|
|
365
|
+
force_function → force_tool
|
|
366
|
+
type: 'function' → type: 'tool_call' (only inside Symposium Message content)
|
|
367
|
+
type: 'function_response' → type: 'tool_result' (only inside Symposium Message content)
|
|
368
|
+
ev.functions → ev.tools (only on tools_auth events)
|
|
369
|
+
```
|
package/Model.js
CHANGED
|
@@ -7,7 +7,14 @@ export default class Model {
|
|
|
7
7
|
return new Map();
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
async generate(model, thread,
|
|
10
|
+
async *generate(model, thread, tools = [], options = {}) {
|
|
11
|
+
// Subclasses implement as async generator: yield deltas during streaming,
|
|
12
|
+
// return assembled Message[] when complete.
|
|
13
|
+
// Delta shape:
|
|
14
|
+
// {type: 'text_delta', content: string}
|
|
15
|
+
// {type: 'reasoning_delta', content: string}
|
|
16
|
+
// {type: 'tool_call', content: {id?, name, arguments}}
|
|
17
|
+
// {type: 'image', content: <image-block-content>, meta}
|
|
11
18
|
return null;
|
|
12
19
|
}
|
|
13
20
|
|
|
@@ -15,55 +22,55 @@ export default class Model {
|
|
|
15
22
|
throw new Error('countTokens not implemented in this model');
|
|
16
23
|
}
|
|
17
24
|
|
|
18
|
-
parseOptions(options = {},
|
|
25
|
+
parseOptions(options = {}, tools = []) {
|
|
19
26
|
options = {
|
|
20
|
-
|
|
21
|
-
|
|
27
|
+
tools: null,
|
|
28
|
+
force_tool: null,
|
|
22
29
|
...options,
|
|
23
30
|
};
|
|
24
31
|
|
|
25
|
-
if (options.
|
|
26
|
-
|
|
32
|
+
if (options.tools !== null)
|
|
33
|
+
tools = options.tools;
|
|
27
34
|
|
|
28
|
-
if (options.
|
|
29
|
-
throw new Error('
|
|
35
|
+
if (options.force_tool && !tools.find(t => t.name === options.force_tool))
|
|
36
|
+
throw new Error('Tool ' + options.force_tool + ' not found.');
|
|
30
37
|
|
|
31
|
-
return {options,
|
|
38
|
+
return {options, tools};
|
|
32
39
|
}
|
|
33
40
|
|
|
34
|
-
|
|
35
|
-
if (options.
|
|
36
|
-
|
|
41
|
+
promptFromTools(options, tools) {
|
|
42
|
+
if (options.force_tool)
|
|
43
|
+
tools = tools.filter(t => t.name !== options.force_tool);
|
|
37
44
|
|
|
38
|
-
if (!
|
|
45
|
+
if (!tools.length)
|
|
39
46
|
return '';
|
|
40
47
|
|
|
41
48
|
let message;
|
|
42
|
-
if (options.
|
|
49
|
+
if (options.force_tool) {
|
|
43
50
|
message = "Nella prossima risposta, rispondi UNICAMENTE seguendo le seguenti istruzioni:\n";
|
|
44
|
-
message +=
|
|
45
|
-
delete
|
|
46
|
-
message += "Rispondi con un messaggio che inizia con le parole:\nCALL " + options.
|
|
51
|
+
message += tools[0].description + "\n";
|
|
52
|
+
delete tools[0].description;
|
|
53
|
+
message += "Rispondi con un messaggio che inizia con le parole:\nCALL " + options.force_tool + "\nE poi a capo un oggetto JSON che segue queste direttive OpenAPI:\n";
|
|
47
54
|
} else {
|
|
48
|
-
message = "Hai a disposizione
|
|
55
|
+
message = "Hai a disposizione alcuni strumenti che puoi chiamare per ottenere risposte o compiere azioni. Ricorda che devi attendere la risposta dello strumento per sapere se ha avuto successo. Per chiamare uno strumento scrivi un messaggio che inizia con CALL nome_strumento e a capo inserisci il JSON con gli argomenti; delimitando il tutto da 3 caratteri ``` - ad esempio:\n" +
|
|
49
56
|
"```\n" +
|
|
50
57
|
"CALL create_user\n" +
|
|
51
58
|
'{"name":"test"}' + "\n" +
|
|
52
59
|
"```\n\n" +
|
|
53
|
-
"Lista
|
|
60
|
+
"Lista degli strumenti che hai a disposizione:\n";
|
|
54
61
|
|
|
55
|
-
for (let
|
|
56
|
-
message += '- ' +
|
|
62
|
+
for (let t of tools)
|
|
63
|
+
message += '- ' + t.name + "\n " + t.description + "\n";
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
message += "\nOpenAPI specs:\n\n";
|
|
60
|
-
for (let
|
|
61
|
-
if (!
|
|
67
|
+
for (let t of tools) {
|
|
68
|
+
if (!t.parameters)
|
|
62
69
|
continue;
|
|
63
|
-
message += '=== ' +
|
|
70
|
+
message += '=== ' + t.name + " ===\n" + JSON.stringify(t.parameters.properties) + "\n\n";
|
|
64
71
|
}
|
|
65
72
|
|
|
66
|
-
if (options.
|
|
73
|
+
if (options.force_tool)
|
|
67
74
|
message += "\nNella risposta non deve esserci NIENTE ALTRO se non queste due cose, non saranno prese in considerazione dal sistema altro genere di risposte.";
|
|
68
75
|
|
|
69
76
|
return message;
|
package/Models/AnthropicModel.js
CHANGED
|
@@ -47,19 +47,19 @@ export default class AnthropicModel extends Model {
|
|
|
47
47
|
return this.anthropic;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
async generate(model, thread,
|
|
50
|
+
async *generate(model, thread, tools = [], options = {}) {
|
|
51
51
|
try {
|
|
52
|
-
const parsed = this.parseOptions(options,
|
|
52
|
+
const parsed = this.parseOptions(options, tools);
|
|
53
53
|
options = parsed.options;
|
|
54
|
-
|
|
54
|
+
tools = parsed.tools;
|
|
55
55
|
|
|
56
56
|
let [system, messages] = await this.convertMessages(thread);
|
|
57
57
|
|
|
58
|
-
if (
|
|
59
|
-
// Se il modello non supporta nativamente
|
|
60
|
-
const
|
|
61
|
-
system += "\n\n" +
|
|
62
|
-
|
|
58
|
+
if (tools.length && !model.tools) {
|
|
59
|
+
// Se il modello non supporta nativamente gli strumenti, aggiungo il prompt al messaggio di sistema
|
|
60
|
+
const tools_prompt = this.promptFromTools(options, tools);
|
|
61
|
+
system += "\n\n" + tools_prompt;
|
|
62
|
+
tools = [];
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
const completion_payload = {
|
|
@@ -72,25 +72,71 @@ export default class AnthropicModel extends Model {
|
|
|
72
72
|
},
|
|
73
73
|
betas: ["interleaved-thinking-2025-05-14"],
|
|
74
74
|
messages,
|
|
75
|
-
tools:
|
|
76
|
-
name:
|
|
77
|
-
description:
|
|
78
|
-
input_schema:
|
|
79
|
-
required:
|
|
75
|
+
tools: tools.map(t => ({
|
|
76
|
+
name: t.name,
|
|
77
|
+
description: t.description,
|
|
78
|
+
input_schema: t.parameters,
|
|
79
|
+
required: t.required || undefined,
|
|
80
80
|
})),
|
|
81
81
|
};
|
|
82
82
|
|
|
83
|
-
if (options.
|
|
83
|
+
if (options.force_tool) {
|
|
84
84
|
completion_payload.tool_choice = {
|
|
85
85
|
type: 'tool',
|
|
86
|
-
name: options.
|
|
86
|
+
name: options.force_tool,
|
|
87
87
|
};
|
|
88
88
|
|
|
89
89
|
delete completion_payload.thinking;
|
|
90
90
|
delete completion_payload.betas;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
const
|
|
93
|
+
const stream = this.getAnthropic().beta.messages.stream(completion_payload);
|
|
94
|
+
|
|
95
|
+
const toolBuffers = new Map();
|
|
96
|
+
|
|
97
|
+
for await (const event of stream) {
|
|
98
|
+
switch (event.type) {
|
|
99
|
+
case 'content_block_start':
|
|
100
|
+
if (event.content_block?.type === 'tool_use') {
|
|
101
|
+
toolBuffers.set(event.index, {
|
|
102
|
+
id: event.content_block.id,
|
|
103
|
+
name: event.content_block.name,
|
|
104
|
+
arguments: '',
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
break;
|
|
108
|
+
|
|
109
|
+
case 'content_block_delta':
|
|
110
|
+
if (event.delta?.type === 'text_delta' && event.delta.text)
|
|
111
|
+
yield {type: 'text_delta', content: event.delta.text};
|
|
112
|
+
else if (event.delta?.type === 'thinking_delta' && event.delta.thinking)
|
|
113
|
+
yield {type: 'reasoning_delta', content: event.delta.thinking};
|
|
114
|
+
else if (event.delta?.type === 'input_json_delta') {
|
|
115
|
+
const buf = toolBuffers.get(event.index);
|
|
116
|
+
if (buf)
|
|
117
|
+
buf.arguments += event.delta.partial_json || '';
|
|
118
|
+
}
|
|
119
|
+
break;
|
|
120
|
+
|
|
121
|
+
case 'content_block_stop': {
|
|
122
|
+
const buf = toolBuffers.get(event.index);
|
|
123
|
+
if (buf) {
|
|
124
|
+
yield {
|
|
125
|
+
type: 'tool_call',
|
|
126
|
+
content: {
|
|
127
|
+
id: buf.id,
|
|
128
|
+
name: buf.name,
|
|
129
|
+
arguments: buf.arguments ? JSON.parse(buf.arguments) : {},
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
toolBuffers.delete(event.index);
|
|
133
|
+
}
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const message = await stream.finalMessage();
|
|
94
140
|
|
|
95
141
|
const message_content = [];
|
|
96
142
|
if (message.content) {
|
|
@@ -102,7 +148,7 @@ export default class AnthropicModel extends Model {
|
|
|
102
148
|
|
|
103
149
|
case 'tool_use':
|
|
104
150
|
message_content.push({
|
|
105
|
-
type: '
|
|
151
|
+
type: 'tool_call',
|
|
106
152
|
content: [
|
|
107
153
|
{
|
|
108
154
|
id: m.id,
|
|
@@ -135,7 +181,7 @@ export default class AnthropicModel extends Model {
|
|
|
135
181
|
console.warn('Rate limite exceeded for Anthropic API, waiting 60 seconds...');
|
|
136
182
|
await new Promise(resolve => setTimeout(resolve, 60000));
|
|
137
183
|
if ((options.counter || 0) < 3)
|
|
138
|
-
return this.generate(model, thread,
|
|
184
|
+
return yield* this.generate(model, thread, tools, {...options, counter: (options.counter || 0) + 1});
|
|
139
185
|
else
|
|
140
186
|
throw new Error('Rate limit exceeded for Anthropic API, aborting.');
|
|
141
187
|
}
|
|
@@ -160,7 +206,7 @@ export default class AnthropicModel extends Model {
|
|
|
160
206
|
});
|
|
161
207
|
break;
|
|
162
208
|
|
|
163
|
-
case '
|
|
209
|
+
case 'tool_call':
|
|
164
210
|
content.push({
|
|
165
211
|
type: 'tool_use',
|
|
166
212
|
name: c.content[0].name,
|
|
@@ -169,7 +215,7 @@ export default class AnthropicModel extends Model {
|
|
|
169
215
|
});
|
|
170
216
|
break;
|
|
171
217
|
|
|
172
|
-
case '
|
|
218
|
+
case 'tool_result':
|
|
173
219
|
content.push({
|
|
174
220
|
type: 'tool_result',
|
|
175
221
|
content: JSON.stringify(c.content.response),
|
package/Models/GrokModel.js
CHANGED
|
@@ -38,9 +38,9 @@ export default class GrokModel extends OpenAIModel {
|
|
|
38
38
|
return this.openai;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
async generate(model, thread,
|
|
41
|
+
async generate(model, thread, tools = [], options = {}) {
|
|
42
42
|
if (options.image_generation) {
|
|
43
|
-
|
|
43
|
+
tools.push({
|
|
44
44
|
name: 'generate_image',
|
|
45
45
|
description: 'Generate an image based on a detailed prompt that you provide',
|
|
46
46
|
parameters: {
|
|
@@ -56,21 +56,21 @@ export default class GrokModel extends OpenAIModel {
|
|
|
56
56
|
});
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
const response = await super.generate(model, thread,
|
|
59
|
+
const response = await super.generate(model, thread, tools, options);
|
|
60
60
|
|
|
61
61
|
// Check for image generation response
|
|
62
62
|
if (options.image_generation) {
|
|
63
|
-
const
|
|
64
|
-
if (
|
|
65
|
-
const generation_call =
|
|
63
|
+
const tool_call_block = response[0].content.find(c => c.type === 'tool_call');
|
|
64
|
+
if (tool_call_block) {
|
|
65
|
+
const generation_call = tool_call_block.content.find(t => t.name === 'generate_image');
|
|
66
66
|
if (generation_call) {
|
|
67
67
|
const response = await this.getOpenAi().images.generate({
|
|
68
68
|
model: 'grok-imagine-image',
|
|
69
69
|
prompt: generation_call.arguments.prompt,
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
tool_call_block.type = 'image';
|
|
73
|
+
tool_call_block.content = {
|
|
74
74
|
type: 'url',
|
|
75
75
|
mime: 'image/jpeg',
|
|
76
76
|
data: response.data[0].url,
|