dtSpark 1.0.4__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.
Files changed (96) hide show
  1. dtSpark/__init__.py +0 -0
  2. dtSpark/_description.txt +1 -0
  3. dtSpark/_full_name.txt +1 -0
  4. dtSpark/_licence.txt +21 -0
  5. dtSpark/_metadata.yaml +6 -0
  6. dtSpark/_name.txt +1 -0
  7. dtSpark/_version.txt +1 -0
  8. dtSpark/aws/__init__.py +7 -0
  9. dtSpark/aws/authentication.py +296 -0
  10. dtSpark/aws/bedrock.py +578 -0
  11. dtSpark/aws/costs.py +318 -0
  12. dtSpark/aws/pricing.py +580 -0
  13. dtSpark/cli_interface.py +2645 -0
  14. dtSpark/conversation_manager.py +3050 -0
  15. dtSpark/core/__init__.py +12 -0
  16. dtSpark/core/application.py +3355 -0
  17. dtSpark/core/context_compaction.py +735 -0
  18. dtSpark/daemon/__init__.py +104 -0
  19. dtSpark/daemon/__main__.py +10 -0
  20. dtSpark/daemon/action_monitor.py +213 -0
  21. dtSpark/daemon/daemon_app.py +730 -0
  22. dtSpark/daemon/daemon_manager.py +289 -0
  23. dtSpark/daemon/execution_coordinator.py +194 -0
  24. dtSpark/daemon/pid_file.py +169 -0
  25. dtSpark/database/__init__.py +482 -0
  26. dtSpark/database/autonomous_actions.py +1191 -0
  27. dtSpark/database/backends.py +329 -0
  28. dtSpark/database/connection.py +122 -0
  29. dtSpark/database/conversations.py +520 -0
  30. dtSpark/database/credential_prompt.py +218 -0
  31. dtSpark/database/files.py +205 -0
  32. dtSpark/database/mcp_ops.py +355 -0
  33. dtSpark/database/messages.py +161 -0
  34. dtSpark/database/schema.py +673 -0
  35. dtSpark/database/tool_permissions.py +186 -0
  36. dtSpark/database/usage.py +167 -0
  37. dtSpark/files/__init__.py +4 -0
  38. dtSpark/files/manager.py +322 -0
  39. dtSpark/launch.py +39 -0
  40. dtSpark/limits/__init__.py +10 -0
  41. dtSpark/limits/costs.py +296 -0
  42. dtSpark/limits/tokens.py +342 -0
  43. dtSpark/llm/__init__.py +17 -0
  44. dtSpark/llm/anthropic_direct.py +446 -0
  45. dtSpark/llm/base.py +146 -0
  46. dtSpark/llm/context_limits.py +438 -0
  47. dtSpark/llm/manager.py +177 -0
  48. dtSpark/llm/ollama.py +578 -0
  49. dtSpark/mcp_integration/__init__.py +5 -0
  50. dtSpark/mcp_integration/manager.py +653 -0
  51. dtSpark/mcp_integration/tool_selector.py +225 -0
  52. dtSpark/resources/config.yaml.template +631 -0
  53. dtSpark/safety/__init__.py +22 -0
  54. dtSpark/safety/llm_service.py +111 -0
  55. dtSpark/safety/patterns.py +229 -0
  56. dtSpark/safety/prompt_inspector.py +442 -0
  57. dtSpark/safety/violation_logger.py +346 -0
  58. dtSpark/scheduler/__init__.py +20 -0
  59. dtSpark/scheduler/creation_tools.py +599 -0
  60. dtSpark/scheduler/execution_queue.py +159 -0
  61. dtSpark/scheduler/executor.py +1152 -0
  62. dtSpark/scheduler/manager.py +395 -0
  63. dtSpark/tools/__init__.py +4 -0
  64. dtSpark/tools/builtin.py +833 -0
  65. dtSpark/web/__init__.py +20 -0
  66. dtSpark/web/auth.py +152 -0
  67. dtSpark/web/dependencies.py +37 -0
  68. dtSpark/web/endpoints/__init__.py +17 -0
  69. dtSpark/web/endpoints/autonomous_actions.py +1125 -0
  70. dtSpark/web/endpoints/chat.py +621 -0
  71. dtSpark/web/endpoints/conversations.py +353 -0
  72. dtSpark/web/endpoints/main_menu.py +547 -0
  73. dtSpark/web/endpoints/streaming.py +421 -0
  74. dtSpark/web/server.py +578 -0
  75. dtSpark/web/session.py +167 -0
  76. dtSpark/web/ssl_utils.py +195 -0
  77. dtSpark/web/static/css/dark-theme.css +427 -0
  78. dtSpark/web/static/js/actions.js +1101 -0
  79. dtSpark/web/static/js/chat.js +614 -0
  80. dtSpark/web/static/js/main.js +496 -0
  81. dtSpark/web/static/js/sse-client.js +242 -0
  82. dtSpark/web/templates/actions.html +408 -0
  83. dtSpark/web/templates/base.html +93 -0
  84. dtSpark/web/templates/chat.html +814 -0
  85. dtSpark/web/templates/conversations.html +350 -0
  86. dtSpark/web/templates/goodbye.html +81 -0
  87. dtSpark/web/templates/login.html +90 -0
  88. dtSpark/web/templates/main_menu.html +983 -0
  89. dtSpark/web/templates/new_conversation.html +191 -0
  90. dtSpark/web/web_interface.py +137 -0
  91. dtspark-1.0.4.dist-info/METADATA +187 -0
  92. dtspark-1.0.4.dist-info/RECORD +96 -0
  93. dtspark-1.0.4.dist-info/WHEEL +5 -0
  94. dtspark-1.0.4.dist-info/entry_points.txt +3 -0
  95. dtspark-1.0.4.dist-info/licenses/LICENSE +21 -0
  96. dtspark-1.0.4.dist-info/top_level.txt +1 -0
dtSpark/llm/ollama.py ADDED
@@ -0,0 +1,578 @@
1
+ """
2
+ Ollama service module for interacting with local Ollama instances.
3
+
4
+ This module provides functionality for:
5
+ - Listing available Ollama models
6
+ - Invoking Ollama models for chat completions
7
+ - Converting between Bedrock and Ollama message formats
8
+ """
9
+
10
+ import json
11
+ import logging
12
+ import os
13
+ import ssl
14
+ import urllib3
15
+ from typing import List, Dict, Optional, Any
16
+ from dtSpark.llm.base import LLMService
17
+ import tiktoken
18
+
19
+ try:
20
+ from ollama import Client
21
+ import httpx
22
+ except ImportError:
23
+ logging.error("ollama module not installed. Please run: pip install ollama")
24
+ raise
25
+
26
+
27
+ class OllamaService(LLMService):
28
+ """Manages interactions with local Ollama instance using official ollama SDK."""
29
+
30
+ def __init__(self, base_url: str = "http://localhost:11434", verify_ssl: bool = True):
31
+ """
32
+ Initialise the Ollama service.
33
+
34
+ Args:
35
+ base_url: Base URL for Ollama API
36
+ verify_ssl: Whether to verify SSL certificates (set to False for self-signed certs)
37
+ """
38
+ self.base_url = base_url.rstrip('/')
39
+ self.verify_ssl = verify_ssl
40
+ self._ssl_warnings_disabled = False
41
+
42
+ # Handle SSL verification settings and create client
43
+ if not verify_ssl:
44
+ logging.info(f"SSL certificate verification disabled for Ollama at {self.base_url}")
45
+ self._disable_ssl_verification()
46
+ self.client = self._create_client_with_ssl_disabled()
47
+ else:
48
+ self.client = Client(host=self.base_url)
49
+
50
+ self.current_model_id = None
51
+ self._verify_connection()
52
+
53
+ def _disable_ssl_verification(self):
54
+ """
55
+ Disable SSL certificate verification for httpx/urllib3.
56
+
57
+ This is necessary when connecting to Ollama instances with self-signed certificates.
58
+ """
59
+ # Suppress InsecureRequestWarning from urllib3
60
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
61
+ self._ssl_warnings_disabled = True
62
+
63
+ # Create an unverified SSL context
64
+ try:
65
+ ssl._create_default_https_context = ssl._create_unverified_context
66
+ except AttributeError:
67
+ # Legacy Python that doesn't support this
68
+ pass
69
+
70
+ def _create_client_with_ssl_disabled(self) -> Client:
71
+ """
72
+ Create an Ollama client with SSL verification disabled.
73
+
74
+ This creates a custom httpx client and injects it into the Ollama client.
75
+ """
76
+ # Ensure base_url has trailing slash for httpx base_url
77
+ base_url = self.base_url
78
+ if not base_url.endswith('/'):
79
+ base_url = base_url + '/'
80
+
81
+ # Create httpx client with SSL verification disabled and proper base URL
82
+ custom_http_client = httpx.Client(
83
+ base_url=base_url,
84
+ verify=False,
85
+ timeout=httpx.Timeout(timeout=120.0)
86
+ )
87
+
88
+ # Create the Ollama client
89
+ client = Client(host=self.base_url)
90
+
91
+ # Monkey-patch the internal httpx client
92
+ # The ollama SDK stores the client as _client
93
+ if hasattr(client, '_client'):
94
+ # Close the original client to free resources
95
+ try:
96
+ client._client.close()
97
+ except Exception:
98
+ pass
99
+ client._client = custom_http_client
100
+ else:
101
+ # Fallback: try to find and replace the httpx client
102
+ for attr_name in dir(client):
103
+ attr = getattr(client, attr_name, None)
104
+ if isinstance(attr, httpx.Client):
105
+ try:
106
+ attr.close()
107
+ except Exception:
108
+ pass
109
+ setattr(client, attr_name, custom_http_client)
110
+ break
111
+
112
+ return client
113
+
114
+ def _verify_connection(self):
115
+ """Verify connection to Ollama instance."""
116
+ try:
117
+ self.client.list()
118
+ logging.info(f"Connected to Ollama at {self.base_url}")
119
+ except Exception as e:
120
+ logging.warning(f"Cannot connect to Ollama at {self.base_url}: {e}")
121
+
122
+ def get_provider_name(self) -> str:
123
+ """Get provider name."""
124
+ return "Ollama"
125
+
126
+ def get_access_info(self) -> str:
127
+ """Get access information."""
128
+ return f"Ollama ({self.base_url})"
129
+
130
+ def list_available_models(self) -> List[Dict[str, Any]]:
131
+ """
132
+ List all available models from Ollama.
133
+
134
+ Returns:
135
+ List of model dictionaries
136
+ """
137
+ try:
138
+ response = self.client.list()
139
+
140
+ models = []
141
+ # Handle both SDK response objects and dict responses
142
+ if hasattr(response, 'models'):
143
+ # SDK response object with attributes
144
+ model_list = response.models
145
+ else:
146
+ # Dict response (for backward compatibility)
147
+ model_list = response.get('models', [])
148
+
149
+ for model in model_list:
150
+ # Handle both SDK model objects and dict models
151
+ if hasattr(model, 'model'):
152
+ # SDK model object
153
+ model_name = model.model
154
+ model_size = model.size if hasattr(model, 'size') else 0
155
+ model_modified = model.modified_at if hasattr(model, 'modified_at') else ''
156
+ else:
157
+ # Dict model
158
+ model_name = model.get('name', '')
159
+ model_size = model.get('size', 0)
160
+ model_modified = model.get('modified_at', '')
161
+
162
+ # Determine tool support based on model capabilities
163
+ supports_tools = self._check_tool_support(model_name)
164
+
165
+ models.append({
166
+ 'id': model_name,
167
+ 'name': model_name,
168
+ 'provider': 'Ollama',
169
+ 'access_info': self.get_access_info(),
170
+ 'supports_tools': supports_tools,
171
+ 'context_length': self._get_context_length(model_name),
172
+ 'size': model_size,
173
+ 'modified': model_modified
174
+ })
175
+
176
+ logging.info(f"Found {len(models)} Ollama models")
177
+ return models
178
+
179
+ except Exception as e:
180
+ logging.error(f"Failed to list Ollama models: {e}")
181
+ return []
182
+
183
+ def _check_tool_support(self, model_name: str) -> bool:
184
+ """
185
+ Check if a model supports tool calling.
186
+
187
+ Args:
188
+ model_name: Name of the model
189
+
190
+ Returns:
191
+ True if model likely supports tools
192
+ """
193
+ # Models known to support function calling
194
+ tool_capable_models = [
195
+ 'llama3.2', 'llama3.1', 'llama3',
196
+ 'mistral', 'mixtral',
197
+ 'qwen2.5', 'qwen2',
198
+ 'command-r',
199
+ ]
200
+
201
+ model_lower = model_name.lower()
202
+ return any(capable in model_lower for capable in tool_capable_models)
203
+
204
+ def _get_context_length(self, model_name: str) -> int:
205
+ """
206
+ Get context length for a model.
207
+
208
+ Args:
209
+ model_name: Name of the model
210
+
211
+ Returns:
212
+ Context length in tokens
213
+ """
214
+ # Common context lengths
215
+ if 'llama3.2' in model_name.lower():
216
+ return 128000
217
+ elif 'llama3.1' in model_name.lower():
218
+ return 128000
219
+ elif 'mistral' in model_name.lower():
220
+ return 32000
221
+ else:
222
+ return 8192 # Default
223
+
224
+ def set_model(self, model_id: str):
225
+ """Set the active Ollama model."""
226
+ self.current_model_id = model_id
227
+ logging.info(f"Ollama model set to: {model_id}")
228
+
229
+ def invoke_model(
230
+ self,
231
+ messages: List[Dict[str, Any]],
232
+ max_tokens: int = 4096,
233
+ temperature: float = 0.7,
234
+ tools: Optional[List[Dict[str, Any]]] = None,
235
+ system: Optional[str] = None,
236
+ max_retries: int = 3
237
+ ) -> Optional[Dict[str, Any]]:
238
+ """
239
+ Invoke Ollama model with conversation.
240
+
241
+ Args:
242
+ messages: Conversation messages
243
+ max_tokens: Maximum tokens to generate
244
+ temperature: Sampling temperature
245
+ tools: Optional tool definitions
246
+ system: Optional system prompt
247
+ max_retries: Maximum retry attempts
248
+
249
+ Returns:
250
+ Response dictionary in standard format
251
+ """
252
+ if not self.current_model_id:
253
+ return {
254
+ 'error': True,
255
+ 'error_code': 'NoModelSelected',
256
+ 'error_message': 'No Ollama model selected',
257
+ 'error_type': 'ConfigurationError'
258
+ }
259
+
260
+ try:
261
+ # Convert messages from Bedrock format to Ollama format
262
+ ollama_messages = self._convert_messages_to_ollama(messages)
263
+
264
+ # Add system message if provided
265
+ if system:
266
+ ollama_messages.insert(0, {
267
+ 'role': 'system',
268
+ 'content': system
269
+ })
270
+
271
+ # Build chat options
272
+ chat_options = {
273
+ 'temperature': temperature,
274
+ 'num_predict': max_tokens
275
+ }
276
+
277
+ logging.debug(f"Invoking Ollama model: {self.current_model_id}")
278
+
279
+ # Use ollama SDK to make the chat request
280
+ if tools and self._check_tool_support(self.current_model_id):
281
+ # Convert tools to Ollama format
282
+ ollama_tools = self._convert_tools_to_ollama(tools)
283
+ response = self.client.chat(
284
+ model=self.current_model_id,
285
+ messages=ollama_messages,
286
+ tools=ollama_tools,
287
+ options=chat_options
288
+ )
289
+ else:
290
+ response = self.client.chat(
291
+ model=self.current_model_id,
292
+ messages=ollama_messages,
293
+ options=chat_options
294
+ )
295
+
296
+ # Convert response to standard format
297
+ return self._convert_response_from_ollama(response)
298
+
299
+ except Exception as e:
300
+ logging.error(f"Ollama API error: {e}")
301
+ return {
302
+ 'error': True,
303
+ 'error_code': 'OllamaAPIError',
304
+ 'error_message': str(e),
305
+ 'error_type': 'RequestError'
306
+ }
307
+
308
+ def _convert_messages_to_ollama(
309
+ self,
310
+ messages: List[Dict[str, Any]]
311
+ ) -> List[Dict[str, Any]]:
312
+ """
313
+ Convert Bedrock message format to Ollama format.
314
+
315
+ Bedrock format:
316
+ {
317
+ 'role': 'user',
318
+ 'content': [{'text': '...'}] or [{'type': 'tool_use', ...}]
319
+ }
320
+
321
+ Ollama format:
322
+ {
323
+ 'role': 'user',
324
+ 'content': '...',
325
+ 'tool_calls': [...] # For assistant messages with tool use
326
+ }
327
+ """
328
+ ollama_messages = []
329
+ # Build a lookup map of tool_use_id -> tool_name for tool result matching
330
+ tool_id_to_name = {}
331
+
332
+ for msg in messages:
333
+ role = msg.get('role', 'user')
334
+ content = msg.get('content', [])
335
+
336
+ # Handle different content formats
337
+ if isinstance(content, str):
338
+ ollama_messages.append({
339
+ 'role': role,
340
+ 'content': content
341
+ })
342
+ elif isinstance(content, list):
343
+ # Extract text and tool blocks
344
+ text_parts = []
345
+ tool_use_blocks = []
346
+ tool_result_blocks = []
347
+
348
+ for block in content:
349
+ if isinstance(block, dict):
350
+ block_type = block.get('type')
351
+
352
+ if block_type == 'text' or 'text' in block:
353
+ text_parts.append(block.get('text', ''))
354
+ elif block_type == 'tool_use':
355
+ tool_use_blocks.append(block)
356
+ elif block_type == 'tool_result':
357
+ tool_result_blocks.append(block)
358
+ elif isinstance(block, str):
359
+ text_parts.append(block)
360
+
361
+ # Build message based on content
362
+ if role == 'assistant' and tool_use_blocks:
363
+ # Assistant message with tool calls
364
+ ollama_msg = {
365
+ 'role': 'assistant',
366
+ 'content': '\n'.join(text_parts) if text_parts else '',
367
+ 'tool_calls': []
368
+ }
369
+
370
+ # Convert tool_use blocks to Ollama tool_calls format
371
+ # Also build mapping of tool_id -> tool_name for later tool result matching
372
+ for tool_block in tool_use_blocks:
373
+ tool_id = tool_block.get('id', '')
374
+ tool_name = tool_block.get('name', '')
375
+
376
+ # Store mapping for tool results
377
+ if tool_id and tool_name:
378
+ tool_id_to_name[tool_id] = tool_name
379
+
380
+ ollama_msg['tool_calls'].append({
381
+ 'id': tool_id,
382
+ 'type': 'function',
383
+ 'function': {
384
+ 'name': tool_name,
385
+ 'arguments': tool_block.get('input', {})
386
+ }
387
+ })
388
+
389
+ ollama_messages.append(ollama_msg)
390
+
391
+ elif role == 'user' and tool_result_blocks:
392
+ # Convert tool results to Ollama SDK format using "tool" role
393
+ # Ollama SDK requires 'tool_name' field (not 'tool_call_id')
394
+ for result_block in tool_result_blocks:
395
+ tool_use_id = result_block.get('tool_use_id', '')
396
+ tool_name = tool_id_to_name.get(tool_use_id, 'unknown_tool')
397
+
398
+ ollama_messages.append({
399
+ 'role': 'tool',
400
+ 'tool_name': tool_name,
401
+ 'content': result_block.get('content', '')
402
+ })
403
+
404
+ # If there's also regular text content, include that as user message
405
+ if text_parts:
406
+ ollama_messages.append({
407
+ 'role': 'user',
408
+ 'content': '\n'.join(text_parts)
409
+ })
410
+ else:
411
+ # Regular message with just text
412
+ text = '\n'.join(text_parts) if text_parts else ''
413
+ ollama_messages.append({
414
+ 'role': role,
415
+ 'content': text
416
+ })
417
+ else:
418
+ ollama_messages.append({
419
+ 'role': role,
420
+ 'content': str(content)
421
+ })
422
+
423
+ return ollama_messages
424
+
425
+ def _convert_tools_to_ollama(
426
+ self,
427
+ tools: List[Dict[str, Any]]
428
+ ) -> List[Dict[str, Any]]:
429
+ """
430
+ Convert Bedrock tool format to Ollama format.
431
+
432
+ Ollama uses OpenAI-compatible function calling format.
433
+ """
434
+ ollama_tools = []
435
+
436
+ for tool in tools:
437
+ # Bedrock tools have 'toolSpec' wrapping
438
+ tool_spec = tool.get('toolSpec', tool)
439
+
440
+ ollama_tools.append({
441
+ 'type': 'function',
442
+ 'function': {
443
+ 'name': tool_spec.get('name', ''),
444
+ 'description': tool_spec.get('description', ''),
445
+ 'parameters': tool_spec.get('inputSchema', {})
446
+ }
447
+ })
448
+
449
+ return ollama_tools
450
+
451
+ def _convert_response_from_ollama(
452
+ self,
453
+ ollama_response: Any
454
+ ) -> Dict[str, Any]:
455
+ """
456
+ Convert Ollama SDK response to standard format.
457
+
458
+ ollama SDK response has attributes:
459
+ - message: with .content, .tool_calls, etc.
460
+ - done: boolean
461
+ """
462
+ # Handle both SDK response objects and dict (for backward compatibility)
463
+ if hasattr(ollama_response, 'message'):
464
+ # SDK response object
465
+ message = ollama_response.message
466
+ content = message.content if hasattr(message, 'content') else ''
467
+ tool_calls = message.tool_calls if hasattr(message, 'tool_calls') else []
468
+ done = ollama_response.done if hasattr(ollama_response, 'done') else True
469
+ else:
470
+ # Dict response (backward compatibility for tests)
471
+ message = ollama_response.get('message', {})
472
+ content = message.get('content', '')
473
+ tool_calls = message.get('tool_calls', [])
474
+ done = ollama_response.get('done', True)
475
+
476
+ # Estimate token usage (Ollama doesn't always provide this)
477
+ input_tokens = self.count_tokens(str(ollama_response))
478
+ output_tokens = self.count_tokens(content if content else '')
479
+
480
+ # Build standard response format
481
+ response = {
482
+ 'stop_reason': 'end_turn' if done else 'max_tokens',
483
+ 'usage': {
484
+ 'input_tokens': input_tokens,
485
+ 'output_tokens': output_tokens
486
+ }
487
+ }
488
+
489
+ # Handle content - build content_blocks array like Bedrock
490
+ content_blocks = []
491
+
492
+ if tool_calls:
493
+ tool_use_blocks = self._convert_tool_calls(tool_calls)
494
+ response['tool_use'] = tool_use_blocks
495
+ response['stop_reason'] = 'tool_use'
496
+
497
+ # Build content_blocks with text (if any) followed by tool calls
498
+ if content:
499
+ # Model provided both text and tool calls
500
+ content_blocks.append({'type': 'text', 'text': content})
501
+ # Add tool use blocks
502
+ content_blocks.extend(tool_use_blocks)
503
+ else:
504
+ # No tool calls, just text content
505
+ if content:
506
+ content_blocks.append({'type': 'text', 'text': content})
507
+
508
+ # Return both formats for compatibility:
509
+ # - content: string (for backward compatibility and text extraction)
510
+ # - content_blocks: array (for conversation manager)
511
+ response['content'] = content
512
+ response['content_blocks'] = content_blocks
513
+
514
+ return response
515
+
516
+ def _convert_tool_calls(
517
+ self,
518
+ tool_calls: List[Any]
519
+ ) -> List[Dict[str, Any]]:
520
+ """Convert Ollama SDK tool calls to standard format."""
521
+ converted = []
522
+
523
+ for call in tool_calls:
524
+ # Handle both SDK objects and dicts (for backward compatibility)
525
+ if hasattr(call, 'function'):
526
+ # SDK response object
527
+ function = call.function
528
+ call_id = call.id if hasattr(call, 'id') else ''
529
+ func_name = function.name if hasattr(function, 'name') else ''
530
+ arguments = function.arguments if hasattr(function, 'arguments') else {}
531
+ else:
532
+ # Dict response (backward compatibility)
533
+ function = call.get('function', {})
534
+ call_id = call.get('id', '')
535
+ func_name = function.get('name', '')
536
+ arguments = function.get('arguments', '{}')
537
+
538
+ # Handle arguments - can be dict or string
539
+ if isinstance(arguments, str):
540
+ # Parse JSON string
541
+ arguments_dict = json.loads(arguments)
542
+ elif isinstance(arguments, dict):
543
+ # Already a dict
544
+ arguments_dict = arguments
545
+ else:
546
+ # Default to empty dict
547
+ arguments_dict = {}
548
+
549
+ converted.append({
550
+ 'type': 'tool_use',
551
+ 'id': call_id,
552
+ 'name': func_name,
553
+ 'input': arguments_dict
554
+ })
555
+
556
+ return converted
557
+
558
+ def supports_streaming(self) -> bool:
559
+ """Check if Ollama supports streaming."""
560
+ return True # Ollama supports streaming, but not implemented yet
561
+
562
+ def count_tokens(self, text: str) -> int:
563
+ """
564
+ Count tokens using tiktoken (approximation for Ollama models).
565
+
566
+ Args:
567
+ text: Text to count tokens for
568
+
569
+ Returns:
570
+ Approximate token count
571
+ """
572
+ try:
573
+ encoding = tiktoken.get_encoding("cl100k_base")
574
+ return len(encoding.encode(text))
575
+ except Exception as e:
576
+ logging.warning(f"Token counting failed: {e}")
577
+ # Fallback: rough estimate of 4 chars per token
578
+ return len(text) // 4
@@ -0,0 +1,5 @@
1
+ """MCP (Model Context Protocol) module."""
2
+ from .manager import MCPManager
3
+ from .tool_selector import ToolSelector
4
+
5
+ __all__ = ['MCPManager', 'ToolSelector']