voxagent 0.1.0__py3-none-any.whl
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.
- voxagent/__init__.py +143 -0
- voxagent/_version.py +5 -0
- voxagent/agent/__init__.py +32 -0
- voxagent/agent/abort.py +178 -0
- voxagent/agent/core.py +902 -0
- voxagent/code/__init__.py +9 -0
- voxagent/mcp/__init__.py +16 -0
- voxagent/mcp/manager.py +188 -0
- voxagent/mcp/tool.py +152 -0
- voxagent/providers/__init__.py +110 -0
- voxagent/providers/anthropic.py +498 -0
- voxagent/providers/augment.py +293 -0
- voxagent/providers/auth.py +116 -0
- voxagent/providers/base.py +268 -0
- voxagent/providers/chatgpt.py +415 -0
- voxagent/providers/claudecode.py +162 -0
- voxagent/providers/cli_base.py +265 -0
- voxagent/providers/codex.py +183 -0
- voxagent/providers/failover.py +90 -0
- voxagent/providers/google.py +532 -0
- voxagent/providers/groq.py +96 -0
- voxagent/providers/ollama.py +425 -0
- voxagent/providers/openai.py +435 -0
- voxagent/providers/registry.py +175 -0
- voxagent/py.typed +1 -0
- voxagent/security/__init__.py +14 -0
- voxagent/security/events.py +75 -0
- voxagent/security/filter.py +169 -0
- voxagent/security/registry.py +87 -0
- voxagent/session/__init__.py +39 -0
- voxagent/session/compaction.py +237 -0
- voxagent/session/lock.py +103 -0
- voxagent/session/model.py +109 -0
- voxagent/session/storage.py +184 -0
- voxagent/streaming/__init__.py +52 -0
- voxagent/streaming/emitter.py +286 -0
- voxagent/streaming/events.py +255 -0
- voxagent/subagent/__init__.py +20 -0
- voxagent/subagent/context.py +124 -0
- voxagent/subagent/definition.py +172 -0
- voxagent/tools/__init__.py +32 -0
- voxagent/tools/context.py +50 -0
- voxagent/tools/decorator.py +175 -0
- voxagent/tools/definition.py +131 -0
- voxagent/tools/executor.py +109 -0
- voxagent/tools/policy.py +89 -0
- voxagent/tools/registry.py +89 -0
- voxagent/types/__init__.py +46 -0
- voxagent/types/messages.py +134 -0
- voxagent/types/run.py +176 -0
- voxagent-0.1.0.dist-info/METADATA +186 -0
- voxagent-0.1.0.dist-info/RECORD +53 -0
- voxagent-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
"""Ollama provider implementation.
|
|
2
|
+
|
|
3
|
+
This module implements the OllamaProvider for local Ollama models,
|
|
4
|
+
supporting NDJSON streaming and tool calling.
|
|
5
|
+
|
|
6
|
+
Ollama is unique:
|
|
7
|
+
- No API key required (local server)
|
|
8
|
+
- NDJSON streaming (not SSE)
|
|
9
|
+
- Dynamic model list from server
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from collections.abc import AsyncIterator
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
import httpx
|
|
17
|
+
|
|
18
|
+
from voxagent.providers.base import (
|
|
19
|
+
AbortSignal,
|
|
20
|
+
BaseProvider,
|
|
21
|
+
ErrorChunk,
|
|
22
|
+
MessageEndChunk,
|
|
23
|
+
StreamChunk,
|
|
24
|
+
TextDeltaChunk,
|
|
25
|
+
ToolUseChunk,
|
|
26
|
+
)
|
|
27
|
+
from voxagent.types import Message, ToolCall
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class OllamaProvider(BaseProvider):
|
|
31
|
+
"""Provider for local Ollama models.
|
|
32
|
+
|
|
33
|
+
Supports streaming via NDJSON, tool calling, and dynamic model discovery.
|
|
34
|
+
No API key required as Ollama runs locally.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
ENV_KEY = "" # No API key needed
|
|
38
|
+
DEFAULT_BASE_URL = "http://localhost:11434"
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
api_key: str | None = None, # Ignored
|
|
43
|
+
base_url: str | None = None,
|
|
44
|
+
model: str = "llama3.3",
|
|
45
|
+
**kwargs: Any,
|
|
46
|
+
) -> None:
|
|
47
|
+
"""Initialize the Ollama provider.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
api_key: Ignored - Ollama doesn't need authentication.
|
|
51
|
+
base_url: Custom base URL for remote Ollama servers.
|
|
52
|
+
model: Model name to use. Defaults to "llama3.3".
|
|
53
|
+
**kwargs: Additional provider-specific arguments.
|
|
54
|
+
"""
|
|
55
|
+
super().__init__(api_key=api_key, base_url=base_url, **kwargs)
|
|
56
|
+
self._model = model
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def name(self) -> str:
|
|
60
|
+
"""Get the provider name."""
|
|
61
|
+
return "ollama"
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def model(self) -> str:
|
|
65
|
+
"""Get the current model name."""
|
|
66
|
+
return self._model
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def models(self) -> list[str]:
|
|
70
|
+
"""Get the list of supported model names.
|
|
71
|
+
|
|
72
|
+
Returns empty list as models are dynamic from server.
|
|
73
|
+
Use list_local_models() to fetch available models.
|
|
74
|
+
"""
|
|
75
|
+
return []
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def supports_tools(self) -> bool:
|
|
79
|
+
"""Check if the provider supports tool/function calling."""
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def supports_streaming(self) -> bool:
|
|
84
|
+
"""Check if the provider supports streaming responses."""
|
|
85
|
+
return True
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def context_limit(self) -> int:
|
|
89
|
+
"""Get the maximum context length in tokens."""
|
|
90
|
+
return 128000
|
|
91
|
+
|
|
92
|
+
def _get_api_endpoint(self) -> str:
|
|
93
|
+
"""Get the API endpoint for chat."""
|
|
94
|
+
base = self._base_url or self.DEFAULT_BASE_URL
|
|
95
|
+
return f"{base}/api/chat"
|
|
96
|
+
|
|
97
|
+
def _get_tags_endpoint(self) -> str:
|
|
98
|
+
"""Get the API endpoint for listing models."""
|
|
99
|
+
base = self._base_url or self.DEFAULT_BASE_URL
|
|
100
|
+
return f"{base}/api/tags"
|
|
101
|
+
|
|
102
|
+
def _convert_messages_to_ollama(
|
|
103
|
+
self, messages: list[Message], system: str | None = None
|
|
104
|
+
) -> list[dict[str, Any]]:
|
|
105
|
+
"""Convert voxagent Messages to Ollama message format.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
messages: List of voxagent Message objects.
|
|
109
|
+
system: Optional system prompt to prepend.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
List of Ollama-format message dictionaries.
|
|
113
|
+
"""
|
|
114
|
+
result: list[dict[str, Any]] = []
|
|
115
|
+
|
|
116
|
+
if system:
|
|
117
|
+
result.append({"role": "system", "content": system})
|
|
118
|
+
|
|
119
|
+
for msg in messages:
|
|
120
|
+
ollama_msg: dict[str, Any] = {"role": msg.role}
|
|
121
|
+
|
|
122
|
+
# Handle content
|
|
123
|
+
if isinstance(msg.content, str):
|
|
124
|
+
ollama_msg["content"] = msg.content
|
|
125
|
+
else:
|
|
126
|
+
# Handle content blocks - convert to string
|
|
127
|
+
text_parts = [b.text for b in msg.content if hasattr(b, "text")]
|
|
128
|
+
ollama_msg["content"] = " ".join(text_parts) if text_parts else ""
|
|
129
|
+
|
|
130
|
+
# Handle tool calls
|
|
131
|
+
if msg.tool_calls:
|
|
132
|
+
ollama_msg["tool_calls"] = [
|
|
133
|
+
{
|
|
134
|
+
"id": tc.id,
|
|
135
|
+
"type": "function",
|
|
136
|
+
"function": {
|
|
137
|
+
"name": tc.name,
|
|
138
|
+
"arguments": tc.params,
|
|
139
|
+
},
|
|
140
|
+
}
|
|
141
|
+
for tc in msg.tool_calls
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
result.append(ollama_msg)
|
|
145
|
+
|
|
146
|
+
return result
|
|
147
|
+
|
|
148
|
+
def _convert_ollama_response_to_message(self, response: dict[str, Any]) -> Message:
|
|
149
|
+
"""Convert Ollama response to voxagent Message.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
response: Ollama API response dictionary.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
A voxagent Message object.
|
|
156
|
+
"""
|
|
157
|
+
msg_data = response.get("message", {})
|
|
158
|
+
role = msg_data.get("role", "assistant")
|
|
159
|
+
content = msg_data.get("content", "")
|
|
160
|
+
|
|
161
|
+
tool_calls: list[ToolCall] | None = None
|
|
162
|
+
if "tool_calls" in msg_data and msg_data["tool_calls"]:
|
|
163
|
+
tool_calls = []
|
|
164
|
+
for tc in msg_data["tool_calls"]:
|
|
165
|
+
func = tc.get("function", {})
|
|
166
|
+
params = func.get("arguments", {})
|
|
167
|
+
tool_calls.append(
|
|
168
|
+
ToolCall(id=tc.get("id", ""), name=func.get("name", ""), params=params)
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
return Message(role=role, content=content, tool_calls=tool_calls)
|
|
172
|
+
|
|
173
|
+
def _parse_ndjson_line(self, data: dict[str, Any]) -> StreamChunk | None:
|
|
174
|
+
"""Parse an NDJSON line and return appropriate StreamChunk.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
data: The parsed JSON data from a line.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
A StreamChunk or None if no chunk should be yielded.
|
|
181
|
+
"""
|
|
182
|
+
# Check for error response
|
|
183
|
+
if "error" in data:
|
|
184
|
+
return ErrorChunk(error=data["error"])
|
|
185
|
+
|
|
186
|
+
msg_data = data.get("message", {})
|
|
187
|
+
done = data.get("done", False)
|
|
188
|
+
|
|
189
|
+
# Check for tool calls first
|
|
190
|
+
if "tool_calls" in msg_data and msg_data["tool_calls"]:
|
|
191
|
+
tc = msg_data["tool_calls"][0] # Process first tool call
|
|
192
|
+
func = tc.get("function", {})
|
|
193
|
+
params = func.get("arguments", {})
|
|
194
|
+
return ToolUseChunk(
|
|
195
|
+
tool_call=ToolCall(
|
|
196
|
+
id=tc.get("id", ""),
|
|
197
|
+
name=func.get("name", ""),
|
|
198
|
+
params=params,
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Check for text content
|
|
203
|
+
content = msg_data.get("content", "")
|
|
204
|
+
if content:
|
|
205
|
+
return TextDeltaChunk(delta=content)
|
|
206
|
+
|
|
207
|
+
# Check if done
|
|
208
|
+
if done:
|
|
209
|
+
return MessageEndChunk()
|
|
210
|
+
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
def _build_request_body(
|
|
214
|
+
self,
|
|
215
|
+
messages: list[Message],
|
|
216
|
+
system: str | None = None,
|
|
217
|
+
tools: list[Any] | None = None,
|
|
218
|
+
stream: bool = False,
|
|
219
|
+
) -> dict[str, Any]:
|
|
220
|
+
"""Build the request body for Ollama API.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
messages: List of voxagent Message objects.
|
|
224
|
+
system: Optional system prompt.
|
|
225
|
+
tools: Optional list of tool definitions.
|
|
226
|
+
stream: Whether to enable streaming.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Request body dictionary.
|
|
230
|
+
"""
|
|
231
|
+
body: dict[str, Any] = {
|
|
232
|
+
"model": self._model,
|
|
233
|
+
"messages": self._convert_messages_to_ollama(messages, system=system),
|
|
234
|
+
"stream": stream,
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if tools:
|
|
238
|
+
body["tools"] = tools
|
|
239
|
+
|
|
240
|
+
return body
|
|
241
|
+
|
|
242
|
+
async def _make_request(
|
|
243
|
+
self,
|
|
244
|
+
messages: list[Message],
|
|
245
|
+
system: str | None = None,
|
|
246
|
+
tools: list[Any] | None = None,
|
|
247
|
+
_endpoint: str | None = None,
|
|
248
|
+
) -> dict[str, Any]:
|
|
249
|
+
"""Make a non-streaming request to the Ollama API.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
messages: List of voxagent Message objects.
|
|
253
|
+
system: Optional system prompt.
|
|
254
|
+
tools: Optional list of tool definitions.
|
|
255
|
+
_endpoint: Internal parameter for endpoint selection ("tags" for model list).
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
The JSON response from the API.
|
|
259
|
+
|
|
260
|
+
Raises:
|
|
261
|
+
Exception: If the API request fails.
|
|
262
|
+
"""
|
|
263
|
+
async with httpx.AsyncClient() as client:
|
|
264
|
+
if _endpoint == "tags":
|
|
265
|
+
# GET request for listing models
|
|
266
|
+
response = await client.get(
|
|
267
|
+
self._get_tags_endpoint(),
|
|
268
|
+
timeout=30.0,
|
|
269
|
+
)
|
|
270
|
+
else:
|
|
271
|
+
# POST request for chat
|
|
272
|
+
body = self._build_request_body(messages, system=system, tools=tools, stream=False)
|
|
273
|
+
response = await client.post(
|
|
274
|
+
self._get_api_endpoint(),
|
|
275
|
+
json=body,
|
|
276
|
+
timeout=120.0,
|
|
277
|
+
)
|
|
278
|
+
response.raise_for_status()
|
|
279
|
+
return response.json()
|
|
280
|
+
|
|
281
|
+
async def _make_streaming_request(
|
|
282
|
+
self,
|
|
283
|
+
messages: list[Message],
|
|
284
|
+
system: str | None = None,
|
|
285
|
+
tools: list[Any] | None = None,
|
|
286
|
+
) -> AsyncIterator[str]:
|
|
287
|
+
"""Make a streaming request to the Ollama API.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
messages: List of voxagent Message objects.
|
|
291
|
+
system: Optional system prompt.
|
|
292
|
+
tools: Optional list of tool definitions.
|
|
293
|
+
|
|
294
|
+
Yields:
|
|
295
|
+
NDJSON lines from the response.
|
|
296
|
+
"""
|
|
297
|
+
body = self._build_request_body(messages, system=system, tools=tools, stream=True)
|
|
298
|
+
|
|
299
|
+
async with httpx.AsyncClient() as client:
|
|
300
|
+
async with client.stream(
|
|
301
|
+
"POST",
|
|
302
|
+
self._get_api_endpoint(),
|
|
303
|
+
json=body,
|
|
304
|
+
timeout=120.0,
|
|
305
|
+
) as response:
|
|
306
|
+
response.raise_for_status()
|
|
307
|
+
async for line in response.aiter_lines():
|
|
308
|
+
if line:
|
|
309
|
+
yield line
|
|
310
|
+
|
|
311
|
+
async def stream(
|
|
312
|
+
self,
|
|
313
|
+
messages: list[Message],
|
|
314
|
+
system: str | None = None,
|
|
315
|
+
tools: list[Any] | None = None,
|
|
316
|
+
abort_signal: AbortSignal | None = None,
|
|
317
|
+
) -> AsyncIterator[StreamChunk]:
|
|
318
|
+
"""Stream a response from the Ollama API.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
messages: The conversation messages.
|
|
322
|
+
system: Optional system prompt.
|
|
323
|
+
tools: Optional list of tool definitions.
|
|
324
|
+
abort_signal: Optional signal to abort the stream.
|
|
325
|
+
|
|
326
|
+
Yields:
|
|
327
|
+
StreamChunk objects containing response data.
|
|
328
|
+
"""
|
|
329
|
+
try:
|
|
330
|
+
async for line in self._make_streaming_request(messages, system=system, tools=tools):
|
|
331
|
+
if abort_signal and abort_signal.aborted:
|
|
332
|
+
return
|
|
333
|
+
|
|
334
|
+
try:
|
|
335
|
+
data = json.loads(line)
|
|
336
|
+
chunk = self._parse_ndjson_line(data)
|
|
337
|
+
if chunk:
|
|
338
|
+
yield chunk
|
|
339
|
+
if isinstance(chunk, MessageEndChunk):
|
|
340
|
+
return
|
|
341
|
+
except json.JSONDecodeError:
|
|
342
|
+
continue
|
|
343
|
+
|
|
344
|
+
except Exception as e:
|
|
345
|
+
yield ErrorChunk(error=str(e))
|
|
346
|
+
|
|
347
|
+
async def complete(
|
|
348
|
+
self,
|
|
349
|
+
messages: list[Message],
|
|
350
|
+
system: str | None = None,
|
|
351
|
+
tools: list[Any] | None = None,
|
|
352
|
+
) -> Message:
|
|
353
|
+
"""Get a complete response from the Ollama API.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
messages: The conversation messages.
|
|
357
|
+
system: Optional system prompt.
|
|
358
|
+
tools: Optional list of tool definitions.
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
The assistant's response message.
|
|
362
|
+
|
|
363
|
+
Raises:
|
|
364
|
+
Exception: If the API request fails.
|
|
365
|
+
"""
|
|
366
|
+
response = await self._make_request(messages, system=system, tools=tools)
|
|
367
|
+
return self._convert_ollama_response_to_message(response)
|
|
368
|
+
|
|
369
|
+
def count_tokens(
|
|
370
|
+
self,
|
|
371
|
+
messages: list[Message],
|
|
372
|
+
system: str | None = None,
|
|
373
|
+
) -> int:
|
|
374
|
+
"""Count tokens in the messages.
|
|
375
|
+
|
|
376
|
+
Uses a simple estimation based on character count.
|
|
377
|
+
Ollama doesn't provide a token counting API.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
messages: The conversation messages.
|
|
381
|
+
system: Optional system prompt.
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
Estimated token count.
|
|
385
|
+
"""
|
|
386
|
+
# Approximate token counting (roughly 4 chars per token for English)
|
|
387
|
+
char_count = 0
|
|
388
|
+
|
|
389
|
+
if system:
|
|
390
|
+
char_count += len(system)
|
|
391
|
+
|
|
392
|
+
for msg in messages:
|
|
393
|
+
if isinstance(msg.content, str):
|
|
394
|
+
char_count += len(msg.content)
|
|
395
|
+
else:
|
|
396
|
+
for block in msg.content:
|
|
397
|
+
if hasattr(block, "text"):
|
|
398
|
+
char_count += len(block.text)
|
|
399
|
+
|
|
400
|
+
# Add overhead for role and structure
|
|
401
|
+
char_count += 10
|
|
402
|
+
|
|
403
|
+
if msg.tool_calls:
|
|
404
|
+
for tc in msg.tool_calls:
|
|
405
|
+
char_count += len(tc.name) + len(json.dumps(tc.params))
|
|
406
|
+
|
|
407
|
+
# Rough estimate: 4 characters per token
|
|
408
|
+
return max(1, char_count // 4)
|
|
409
|
+
|
|
410
|
+
async def list_local_models(self) -> list[str]:
|
|
411
|
+
"""List locally available models from Ollama server.
|
|
412
|
+
|
|
413
|
+
Returns:
|
|
414
|
+
List of model names available on the server.
|
|
415
|
+
|
|
416
|
+
Raises:
|
|
417
|
+
Exception: If the server connection fails.
|
|
418
|
+
"""
|
|
419
|
+
response = await self._make_request(messages=[], _endpoint="tags")
|
|
420
|
+
models = response.get("models", [])
|
|
421
|
+
return [m.get("name", "") for m in models if m.get("name")]
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
__all__ = ["OllamaProvider"]
|
|
425
|
+
|