quantum-framework 0.9.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.
- quantum/__init__.py +1 -0
- quantum/cli/__init__.py +2 -0
- quantum/cli/commands/__init__.py +15 -0
- quantum/cli/commands/build.py +417 -0
- quantum/cli/commands/dev.py +185 -0
- quantum/cli/commands/docs.py +329 -0
- quantum/cli/commands/lint.py +523 -0
- quantum/cli/commands/migrate.py +527 -0
- quantum/cli/commands/new.py +622 -0
- quantum/cli/commands/serve.py +190 -0
- quantum/cli/commands/test.py +193 -0
- quantum/cli/deploy.py +810 -0
- quantum/cli/hot_reload.py +951 -0
- quantum/cli/jobs.py +356 -0
- quantum/cli/mq.py +582 -0
- quantum/cli/pkg.py +390 -0
- quantum/cli/runner.py +547 -0
- quantum/cli/server_process.py +159 -0
- quantum/cli/utils.py +334 -0
- quantum/compiler/__init__.py +30 -0
- quantum/compiler/base_generator.py +367 -0
- quantum/compiler/cli.py +295 -0
- quantum/compiler/expression_transformer.py +444 -0
- quantum/compiler/javascript/__init__.py +10 -0
- quantum/compiler/javascript/generator.py +659 -0
- quantum/compiler/optimizer.py +270 -0
- quantum/compiler/python/__init__.py +10 -0
- quantum/compiler/python/generator.py +883 -0
- quantum/compiler/python/runtime.py +863 -0
- quantum/compiler/transpiler.py +330 -0
- quantum/core/__init__.py +3 -0
- quantum/core/ast_nodes.py +2611 -0
- quantum/core/expression_diagnostics.py +87 -0
- quantum/core/expression_stdlib.py +148 -0
- quantum/core/expressions.py +591 -0
- quantum/core/features/agents/src/__init__.py +30 -0
- quantum/core/features/agents/src/ast_node.py +540 -0
- quantum/core/features/conditionals/src/__init__.py +8 -0
- quantum/core/features/conditionals/src/ast_node.py +68 -0
- quantum/core/features/data_fetching/src/__init__.py +21 -0
- quantum/core/features/data_fetching/src/ast_node.py +312 -0
- quantum/core/features/data_fetching/src/desktop_adapter.py +351 -0
- quantum/core/features/data_fetching/src/html_adapter.py +474 -0
- quantum/core/features/data_fetching/src/parser.py +225 -0
- quantum/core/features/data_import/src/__init__.py +0 -0
- quantum/core/features/data_import/src/ast_node.py +291 -0
- quantum/core/features/data_import/src/runtime.py +538 -0
- quantum/core/features/dump/src/__init__.py +12 -0
- quantum/core/features/dump/src/ast_node.py +106 -0
- quantum/core/features/dump/src/parser.py +61 -0
- quantum/core/features/dump/src/runtime.py +246 -0
- quantum/core/features/functions/src/__init__.py +8 -0
- quantum/core/features/functions/src/ast_node.py +149 -0
- quantum/core/features/game_engine_2d/src/__init__.py +19 -0
- quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
- quantum/core/features/game_engine_2d/src/parser.py +983 -0
- quantum/core/features/invocation/src/__init__.py +0 -0
- quantum/core/features/invocation/src/ast_node.py +146 -0
- quantum/core/features/invocation/src/runtime.py +327 -0
- quantum/core/features/knowledge_base/src/__init__.py +6 -0
- quantum/core/features/knowledge_base/src/ast_node.py +113 -0
- quantum/core/features/knowledge_base/src/parser.py +82 -0
- quantum/core/features/logging/src/__init__.py +12 -0
- quantum/core/features/logging/src/ast_node.py +111 -0
- quantum/core/features/logging/src/parser.py +50 -0
- quantum/core/features/logging/src/runtime.py +190 -0
- quantum/core/features/loops/src/__init__.py +8 -0
- quantum/core/features/loops/src/ast_node.py +60 -0
- quantum/core/features/query/src/__init__.py +0 -0
- quantum/core/features/query/src/database_service.py +322 -0
- quantum/core/features/query/src/query_validators.py +20 -0
- quantum/core/features/state_management/src/__init__.py +11 -0
- quantum/core/features/state_management/src/ast_node.py +228 -0
- quantum/core/features/terminal_engine/src/__init__.py +21 -0
- quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
- quantum/core/features/terminal_engine/src/parser.py +361 -0
- quantum/core/features/testing_engine/src/__init__.py +41 -0
- quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
- quantum/core/features/testing_engine/src/parser.py +604 -0
- quantum/core/features/theming/src/__init__.py +48 -0
- quantum/core/features/theming/src/ast_node.py +137 -0
- quantum/core/features/theming/src/presets.py +405 -0
- quantum/core/features/ui_engine/src/__init__.py +20 -0
- quantum/core/features/ui_engine/src/ast_nodes.py +1854 -0
- quantum/core/features/ui_engine/src/parser.py +1106 -0
- quantum/core/features/websocket/src/__init__.py +24 -0
- quantum/core/features/websocket/src/ast_node.py +247 -0
- quantum/core/html_compat.py +299 -0
- quantum/core/parser.py +1235 -0
- quantum/core/parser_registry.py +213 -0
- quantum/core/parsers/__init__.py +76 -0
- quantum/core/parsers/ai/__init__.py +12 -0
- quantum/core/parsers/ai/agent_parser.py +106 -0
- quantum/core/parsers/ai/knowledge_parser.py +86 -0
- quantum/core/parsers/ai/llm_parser.py +78 -0
- quantum/core/parsers/ai/team_parser.py +88 -0
- quantum/core/parsers/base.py +322 -0
- quantum/core/parsers/composition/__init__.py +10 -0
- quantum/core/parsers/composition/import_parser.py +50 -0
- quantum/core/parsers/composition/slot_parser.py +48 -0
- quantum/core/parsers/control_flow/__init__.py +11 -0
- quantum/core/parsers/control_flow/if_parser.py +68 -0
- quantum/core/parsers/control_flow/loop_parser.py +105 -0
- quantum/core/parsers/control_flow/set_parser.py +89 -0
- quantum/core/parsers/data/__init__.py +12 -0
- quantum/core/parsers/data/data_parser.py +217 -0
- quantum/core/parsers/data/invoke_parser.py +116 -0
- quantum/core/parsers/data/query_parser.py +188 -0
- quantum/core/parsers/data/transaction_parser.py +77 -0
- quantum/core/parsers/events/__init__.py +9 -0
- quantum/core/parsers/events/dispatch_event_parser.py +44 -0
- quantum/core/parsers/forms/__init__.py +11 -0
- quantum/core/parsers/forms/action_parser.py +54 -0
- quantum/core/parsers/forms/flash_parser.py +34 -0
- quantum/core/parsers/forms/redirect_parser.py +33 -0
- quantum/core/parsers/functions/__init__.py +11 -0
- quantum/core/parsers/functions/function_parser.py +137 -0
- quantum/core/parsers/functions/param_parser.py +66 -0
- quantum/core/parsers/functions/return_parser.py +31 -0
- quantum/core/parsers/html/__init__.py +10 -0
- quantum/core/parsers/html/component_call_parser.py +130 -0
- quantum/core/parsers/html/html_parser.py +115 -0
- quantum/core/parsers/jobs/__init__.py +11 -0
- quantum/core/parsers/jobs/job_parser.py +71 -0
- quantum/core/parsers/jobs/schedule_parser.py +62 -0
- quantum/core/parsers/jobs/thread_parser.py +57 -0
- quantum/core/parsers/messaging/__init__.py +17 -0
- quantum/core/parsers/messaging/message_ack_parser.py +30 -0
- quantum/core/parsers/messaging/message_nack_parser.py +30 -0
- quantum/core/parsers/messaging/message_parser.py +114 -0
- quantum/core/parsers/messaging/queue_parser.py +61 -0
- quantum/core/parsers/messaging/websocket_parser.py +121 -0
- quantum/core/parsers/persistence/__init__.py +9 -0
- quantum/core/parsers/persistence/persist_parser.py +64 -0
- quantum/core/parsers/routing/__init__.py +9 -0
- quantum/core/parsers/routing/route_parser.py +41 -0
- quantum/core/parsers/scripting/__init__.py +12 -0
- quantum/core/parsers/scripting/pyclass_parser.py +62 -0
- quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
- quantum/core/parsers/scripting/pyimport_parser.py +52 -0
- quantum/core/parsers/scripting/python_parser.py +49 -0
- quantum/core/parsers/services/__init__.py +12 -0
- quantum/core/parsers/services/dump_parser.py +52 -0
- quantum/core/parsers/services/file_parser.py +48 -0
- quantum/core/parsers/services/log_parser.py +46 -0
- quantum/core/parsers/services/mail_parser.py +65 -0
- quantum/core/tiers.py +82 -0
- quantum/packages/__init__.py +28 -0
- quantum/packages/manager.py +413 -0
- quantum/packages/manifest.py +351 -0
- quantum/packages/registry.py +399 -0
- quantum/packages/resolver.py +336 -0
- quantum/plugins/__init__.py +33 -0
- quantum/plugins/hooks.py +329 -0
- quantum/plugins/loader.py +479 -0
- quantum/plugins/manifest.py +336 -0
- quantum/plugins/registry.py +371 -0
- quantum/runtime/__init__.py +28 -0
- quantum/runtime/action_handler.py +443 -0
- quantum/runtime/adapters/__init__.py +88 -0
- quantum/runtime/adapters/memory_adapter.py +690 -0
- quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
- quantum/runtime/adapters/redis_adapter.py +582 -0
- quantum/runtime/adapters/sqlite_adapter.py +414 -0
- quantum/runtime/agent_service.py +1133 -0
- quantum/runtime/api_server.py +86 -0
- quantum/runtime/ast_cache.py +506 -0
- quantum/runtime/auth_service.py +267 -0
- quantum/runtime/component.py +990 -0
- quantum/runtime/component_composer.py +319 -0
- quantum/runtime/component_resolver.py +174 -0
- quantum/runtime/database_service.py +598 -0
- quantum/runtime/email_service.py +162 -0
- quantum/runtime/error_handler.py +295 -0
- quantum/runtime/execution_context.py +286 -0
- quantum/runtime/executor_registry.py +171 -0
- quantum/runtime/executors/__init__.py +71 -0
- quantum/runtime/executors/ai/__init__.py +12 -0
- quantum/runtime/executors/ai/agent_executor.py +217 -0
- quantum/runtime/executors/ai/knowledge_executor.py +114 -0
- quantum/runtime/executors/ai/llm_executor.py +153 -0
- quantum/runtime/executors/ai/team_executor.py +171 -0
- quantum/runtime/executors/base.py +262 -0
- quantum/runtime/executors/control_flow/__init__.py +11 -0
- quantum/runtime/executors/control_flow/if_executor.py +93 -0
- quantum/runtime/executors/control_flow/loop_executor.py +307 -0
- quantum/runtime/executors/control_flow/set_executor.py +412 -0
- quantum/runtime/executors/data/__init__.py +12 -0
- quantum/runtime/executors/data/data_executor.py +145 -0
- quantum/runtime/executors/data/invoke_executor.py +176 -0
- quantum/runtime/executors/data/query_executor.py +256 -0
- quantum/runtime/executors/data/transaction_executor.py +91 -0
- quantum/runtime/executors/jobs/__init__.py +11 -0
- quantum/runtime/executors/jobs/job_executor.py +190 -0
- quantum/runtime/executors/jobs/schedule_executor.py +132 -0
- quantum/runtime/executors/jobs/thread_executor.py +127 -0
- quantum/runtime/executors/messaging/__init__.py +17 -0
- quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
- quantum/runtime/executors/messaging/message_executor.py +174 -0
- quantum/runtime/executors/messaging/queue_executor.py +103 -0
- quantum/runtime/executors/messaging/websocket_executor.py +197 -0
- quantum/runtime/executors/scripting/__init__.py +11 -0
- quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
- quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
- quantum/runtime/executors/scripting/python_executor.py +249 -0
- quantum/runtime/executors/services/__init__.py +12 -0
- quantum/runtime/executors/services/dump_executor.py +72 -0
- quantum/runtime/executors/services/file_executor.py +89 -0
- quantum/runtime/executors/services/log_executor.py +77 -0
- quantum/runtime/executors/services/mail_executor.py +81 -0
- quantum/runtime/expression_cache.py +498 -0
- quantum/runtime/file_upload_service.py +326 -0
- quantum/runtime/function_registry.py +118 -0
- quantum/runtime/game_builder.py +166 -0
- quantum/runtime/game_code_generator.py +2371 -0
- quantum/runtime/game_templates.py +2006 -0
- quantum/runtime/godot_code_generator.py +4681 -0
- quantum/runtime/godot_templates.py +1449 -0
- quantum/runtime/job_executor.py +1599 -0
- quantum/runtime/knowledge_service.py +500 -0
- quantum/runtime/llm_cache.py +100 -0
- quantum/runtime/llm_providers.py +704 -0
- quantum/runtime/llm_service.py +287 -0
- quantum/runtime/logging_setup.py +140 -0
- quantum/runtime/message_broker.py +364 -0
- quantum/runtime/message_queue_service.py +571 -0
- quantum/runtime/param_validation.py +184 -0
- quantum/runtime/pypy_compat.py +315 -0
- quantum/runtime/python_bridge.py +698 -0
- quantum/runtime/query_validators.py +304 -0
- quantum/runtime/renderer.py +733 -0
- quantum/runtime/service_container.py +444 -0
- quantum/runtime/terminal_builder.py +76 -0
- quantum/runtime/terminal_code_generator.py +607 -0
- quantum/runtime/terminal_templates.py +243 -0
- quantum/runtime/testing_builder.py +77 -0
- quantum/runtime/testing_code_generator.py +833 -0
- quantum/runtime/testing_templates.py +85 -0
- quantum/runtime/ui_builder.py +188 -0
- quantum/runtime/ui_desktop_adapter.py +1730 -0
- quantum/runtime/ui_desktop_templates.py +307 -0
- quantum/runtime/ui_html_adapter.py +2691 -0
- quantum/runtime/ui_html_templates.py +2297 -0
- quantum/runtime/ui_mobile_adapter.py +1832 -0
- quantum/runtime/ui_mobile_templates.py +1003 -0
- quantum/runtime/ui_textual_adapter.py +1866 -0
- quantum/runtime/ui_textual_templates.py +45 -0
- quantum/runtime/ui_tokens.py +465 -0
- quantum/runtime/ui_validator.py +365 -0
- quantum/runtime/validators.py +256 -0
- quantum/runtime/web_server.py +1766 -0
- quantum/runtime/websocket_adapter.py +501 -0
- quantum/runtime/websocket_service.py +585 -0
- quantum/runtime/websocket_transport.py +289 -0
- quantum/runtime/wsgi.py +101 -0
- quantum/utils/__init__.py +1 -0
- quantum_framework-0.9.0.dist-info/METADATA +244 -0
- quantum_framework-0.9.0.dist-info/RECORD +262 -0
- quantum_framework-0.9.0.dist-info/WHEEL +5 -0
- quantum_framework-0.9.0.dist-info/entry_points.txt +2 -0
- quantum_framework-0.9.0.dist-info/licenses/LICENSE +21 -0
- quantum_framework-0.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Quantum LLM Providers - Multi-provider LLM support
|
|
3
|
+
|
|
4
|
+
Supports:
|
|
5
|
+
- Ollama (local, default)
|
|
6
|
+
- LM Studio (local, OpenAI-compatible)
|
|
7
|
+
- OpenAI / ChatGPT (cloud)
|
|
8
|
+
- Anthropic / Claude (cloud)
|
|
9
|
+
|
|
10
|
+
Each provider implements a common interface for chat/generate operations.
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
from quantum.runtime.llm_providers import get_llm_provider, LLMProvider
|
|
14
|
+
|
|
15
|
+
# Auto-detect provider from endpoint
|
|
16
|
+
provider = get_llm_provider(endpoint="http://localhost:11434") # Ollama
|
|
17
|
+
provider = get_llm_provider(endpoint="http://localhost:1234/v1") # LM Studio
|
|
18
|
+
provider = get_llm_provider(provider="openai", api_key="sk-...") # OpenAI
|
|
19
|
+
provider = get_llm_provider(provider="anthropic", api_key="sk-ant-...") # Claude
|
|
20
|
+
|
|
21
|
+
# Use common interface
|
|
22
|
+
result = provider.chat(messages=[...], model="gpt-4")
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import os
|
|
26
|
+
import json
|
|
27
|
+
import logging
|
|
28
|
+
from abc import ABC, abstractmethod
|
|
29
|
+
from typing import List, Dict, Any, Optional
|
|
30
|
+
from dataclasses import dataclass
|
|
31
|
+
from enum import Enum
|
|
32
|
+
|
|
33
|
+
import requests
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ProviderType(Enum):
|
|
39
|
+
"""Supported LLM providers."""
|
|
40
|
+
OLLAMA = "ollama"
|
|
41
|
+
OPENAI = "openai" # Also works for LM Studio
|
|
42
|
+
ANTHROPIC = "anthropic"
|
|
43
|
+
AUTO = "auto" # Auto-detect from endpoint
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class LLMProviderError(Exception):
|
|
47
|
+
"""Error from LLM provider."""
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class LLMResponse:
|
|
53
|
+
"""Unified response from any LLM provider."""
|
|
54
|
+
success: bool
|
|
55
|
+
content: str
|
|
56
|
+
model: str
|
|
57
|
+
provider: str
|
|
58
|
+
usage: Optional[Dict[str, int]] = None
|
|
59
|
+
error: Optional[str] = None
|
|
60
|
+
raw: Optional[Dict[str, Any]] = None
|
|
61
|
+
|
|
62
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
63
|
+
return {
|
|
64
|
+
"success": self.success,
|
|
65
|
+
"data": self.content,
|
|
66
|
+
"content": self.content,
|
|
67
|
+
"model": self.model,
|
|
68
|
+
"provider": self.provider,
|
|
69
|
+
"usage": self.usage,
|
|
70
|
+
"error": self.error,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class BaseLLMProvider(ABC):
|
|
75
|
+
"""Base class for LLM providers."""
|
|
76
|
+
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
base_url: str,
|
|
80
|
+
api_key: Optional[str] = None,
|
|
81
|
+
default_model: Optional[str] = None,
|
|
82
|
+
timeout: int = 60
|
|
83
|
+
):
|
|
84
|
+
self.base_url = base_url.rstrip('/')
|
|
85
|
+
self.api_key = api_key
|
|
86
|
+
self.default_model = default_model
|
|
87
|
+
self.timeout = timeout
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
@abstractmethod
|
|
91
|
+
def provider_name(self) -> str:
|
|
92
|
+
"""Provider identifier."""
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
@abstractmethod
|
|
96
|
+
def chat(
|
|
97
|
+
self,
|
|
98
|
+
messages: List[Dict[str, str]],
|
|
99
|
+
model: Optional[str] = None,
|
|
100
|
+
temperature: Optional[float] = None,
|
|
101
|
+
max_tokens: Optional[int] = None,
|
|
102
|
+
**kwargs
|
|
103
|
+
) -> LLMResponse:
|
|
104
|
+
"""Send chat completion request."""
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
def generate(
|
|
108
|
+
self,
|
|
109
|
+
prompt: str,
|
|
110
|
+
model: Optional[str] = None,
|
|
111
|
+
system: Optional[str] = None,
|
|
112
|
+
**kwargs
|
|
113
|
+
) -> LLMResponse:
|
|
114
|
+
"""Generate completion from prompt (converts to chat format)."""
|
|
115
|
+
messages = []
|
|
116
|
+
if system:
|
|
117
|
+
messages.append({"role": "system", "content": system})
|
|
118
|
+
messages.append({"role": "user", "content": prompt})
|
|
119
|
+
return self.chat(messages, model=model, **kwargs)
|
|
120
|
+
|
|
121
|
+
def _get_headers(self) -> Dict[str, str]:
|
|
122
|
+
"""Get request headers."""
|
|
123
|
+
headers = {"Content-Type": "application/json"}
|
|
124
|
+
if self.api_key:
|
|
125
|
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
126
|
+
return headers
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class OllamaProvider(BaseLLMProvider):
|
|
130
|
+
"""
|
|
131
|
+
Ollama provider - local LLM inference.
|
|
132
|
+
|
|
133
|
+
Endpoints:
|
|
134
|
+
- /api/generate (completion)
|
|
135
|
+
- /api/chat (chat)
|
|
136
|
+
- /api/tags (list models)
|
|
137
|
+
- /api/embed (embeddings)
|
|
138
|
+
|
|
139
|
+
Default URL: http://localhost:11434
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def provider_name(self) -> str:
|
|
144
|
+
return "ollama"
|
|
145
|
+
|
|
146
|
+
def __init__(
|
|
147
|
+
self,
|
|
148
|
+
base_url: str = None,
|
|
149
|
+
default_model: str = None,
|
|
150
|
+
timeout: int = 60,
|
|
151
|
+
**kwargs
|
|
152
|
+
):
|
|
153
|
+
base_url = base_url or os.getenv('QUANTUM_LLM_BASE_URL', 'http://localhost:11434')
|
|
154
|
+
default_model = default_model or os.getenv('QUANTUM_LLM_DEFAULT_MODEL', 'phi3')
|
|
155
|
+
super().__init__(base_url, None, default_model, timeout)
|
|
156
|
+
|
|
157
|
+
def chat(
|
|
158
|
+
self,
|
|
159
|
+
messages: List[Dict[str, str]],
|
|
160
|
+
model: Optional[str] = None,
|
|
161
|
+
temperature: Optional[float] = None,
|
|
162
|
+
max_tokens: Optional[int] = None,
|
|
163
|
+
response_format: Optional[str] = None,
|
|
164
|
+
**kwargs
|
|
165
|
+
) -> LLMResponse:
|
|
166
|
+
"""Send chat request to Ollama."""
|
|
167
|
+
model = model or self.default_model
|
|
168
|
+
|
|
169
|
+
payload = {
|
|
170
|
+
"model": model,
|
|
171
|
+
"messages": messages,
|
|
172
|
+
"stream": False,
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
options = {}
|
|
176
|
+
if temperature is not None:
|
|
177
|
+
options["temperature"] = temperature
|
|
178
|
+
if max_tokens is not None:
|
|
179
|
+
options["num_predict"] = max_tokens
|
|
180
|
+
if options:
|
|
181
|
+
payload["options"] = options
|
|
182
|
+
|
|
183
|
+
if response_format == "json":
|
|
184
|
+
payload["format"] = "json"
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
url = f"{self.base_url}/api/chat"
|
|
188
|
+
logger.debug(f"Ollama chat: model={model}, messages={len(messages)}")
|
|
189
|
+
|
|
190
|
+
resp = requests.post(url, json=payload, timeout=self.timeout)
|
|
191
|
+
resp.raise_for_status()
|
|
192
|
+
|
|
193
|
+
data = resp.json()
|
|
194
|
+
message = data.get("message", {})
|
|
195
|
+
content = message.get("content", "")
|
|
196
|
+
|
|
197
|
+
return LLMResponse(
|
|
198
|
+
success=True,
|
|
199
|
+
content=content,
|
|
200
|
+
model=data.get("model", model),
|
|
201
|
+
provider=self.provider_name,
|
|
202
|
+
usage={
|
|
203
|
+
"prompt_tokens": data.get("prompt_eval_count", 0),
|
|
204
|
+
"completion_tokens": data.get("eval_count", 0),
|
|
205
|
+
"total_tokens": data.get("prompt_eval_count", 0) + data.get("eval_count", 0)
|
|
206
|
+
},
|
|
207
|
+
raw=data
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
except requests.ConnectionError:
|
|
211
|
+
raise LLMProviderError(
|
|
212
|
+
f"Cannot connect to Ollama at {self.base_url}. "
|
|
213
|
+
"Ensure Ollama is running: ollama serve"
|
|
214
|
+
)
|
|
215
|
+
except requests.Timeout:
|
|
216
|
+
raise LLMProviderError(f"Ollama request timed out after {self.timeout}s")
|
|
217
|
+
except requests.HTTPError as e:
|
|
218
|
+
raise LLMProviderError(f"Ollama error: {e.response.status_code} - {e.response.text}")
|
|
219
|
+
except Exception as e:
|
|
220
|
+
raise LLMProviderError(f"Ollama error: {e}")
|
|
221
|
+
|
|
222
|
+
def list_models(self) -> List[str]:
|
|
223
|
+
"""List available Ollama models."""
|
|
224
|
+
try:
|
|
225
|
+
resp = requests.get(f"{self.base_url}/api/tags", timeout=10)
|
|
226
|
+
resp.raise_for_status()
|
|
227
|
+
data = resp.json()
|
|
228
|
+
return [m.get("name", "") for m in data.get("models", [])]
|
|
229
|
+
except Exception as e:
|
|
230
|
+
logger.error(f"Error listing Ollama models: {e}")
|
|
231
|
+
return []
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class OpenAIProvider(BaseLLMProvider):
|
|
235
|
+
"""
|
|
236
|
+
OpenAI-compatible provider.
|
|
237
|
+
|
|
238
|
+
Works with:
|
|
239
|
+
- OpenAI API (api.openai.com)
|
|
240
|
+
- LM Studio (localhost:1234)
|
|
241
|
+
- Azure OpenAI
|
|
242
|
+
- Any OpenAI-compatible API
|
|
243
|
+
|
|
244
|
+
Endpoints:
|
|
245
|
+
- /v1/chat/completions (chat)
|
|
246
|
+
- /v1/completions (legacy)
|
|
247
|
+
- /v1/embeddings (embeddings)
|
|
248
|
+
|
|
249
|
+
Default URL: https://api.openai.com (or localhost:1234 for LM Studio)
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
@property
|
|
253
|
+
def provider_name(self) -> str:
|
|
254
|
+
return "openai"
|
|
255
|
+
|
|
256
|
+
def __init__(
|
|
257
|
+
self,
|
|
258
|
+
base_url: str = None,
|
|
259
|
+
api_key: str = None,
|
|
260
|
+
default_model: str = None,
|
|
261
|
+
timeout: int = 60,
|
|
262
|
+
**kwargs
|
|
263
|
+
):
|
|
264
|
+
# Check for LM Studio first (local), then OpenAI
|
|
265
|
+
if base_url is None:
|
|
266
|
+
# Try to detect LM Studio
|
|
267
|
+
lm_studio_url = os.getenv('LM_STUDIO_URL', 'http://localhost:1234/v1')
|
|
268
|
+
openai_url = os.getenv('OPENAI_API_BASE', 'https://api.openai.com/v1')
|
|
269
|
+
base_url = lm_studio_url if self._is_local_available(lm_studio_url) else openai_url
|
|
270
|
+
|
|
271
|
+
api_key = api_key or os.getenv('OPENAI_API_KEY', '')
|
|
272
|
+
default_model = default_model or os.getenv('OPENAI_MODEL', 'gpt-3.5-turbo')
|
|
273
|
+
|
|
274
|
+
super().__init__(base_url, api_key, default_model, timeout)
|
|
275
|
+
|
|
276
|
+
def _is_local_available(self, url: str) -> bool:
|
|
277
|
+
"""Check if local LM Studio is running."""
|
|
278
|
+
try:
|
|
279
|
+
resp = requests.get(f"{url}/models", timeout=2)
|
|
280
|
+
return resp.status_code == 200
|
|
281
|
+
except:
|
|
282
|
+
return False
|
|
283
|
+
|
|
284
|
+
def chat(
|
|
285
|
+
self,
|
|
286
|
+
messages: List[Dict[str, str]],
|
|
287
|
+
model: Optional[str] = None,
|
|
288
|
+
temperature: Optional[float] = None,
|
|
289
|
+
max_tokens: Optional[int] = None,
|
|
290
|
+
response_format: Optional[str] = None,
|
|
291
|
+
**kwargs
|
|
292
|
+
) -> LLMResponse:
|
|
293
|
+
"""Send chat request to OpenAI-compatible API."""
|
|
294
|
+
model = model or self.default_model
|
|
295
|
+
|
|
296
|
+
payload = {
|
|
297
|
+
"model": model,
|
|
298
|
+
"messages": messages,
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if temperature is not None:
|
|
302
|
+
payload["temperature"] = temperature
|
|
303
|
+
if max_tokens is not None:
|
|
304
|
+
payload["max_tokens"] = max_tokens
|
|
305
|
+
if response_format == "json":
|
|
306
|
+
payload["response_format"] = {"type": "json_object"}
|
|
307
|
+
|
|
308
|
+
try:
|
|
309
|
+
url = f"{self.base_url}/chat/completions"
|
|
310
|
+
if not url.startswith("http"):
|
|
311
|
+
url = f"https://{url}"
|
|
312
|
+
|
|
313
|
+
logger.debug(f"OpenAI chat: model={model}, url={url}")
|
|
314
|
+
|
|
315
|
+
resp = requests.post(
|
|
316
|
+
url,
|
|
317
|
+
json=payload,
|
|
318
|
+
headers=self._get_headers(),
|
|
319
|
+
timeout=self.timeout
|
|
320
|
+
)
|
|
321
|
+
resp.raise_for_status()
|
|
322
|
+
|
|
323
|
+
data = resp.json()
|
|
324
|
+
choice = data.get("choices", [{}])[0]
|
|
325
|
+
message = choice.get("message", {})
|
|
326
|
+
content = message.get("content", "")
|
|
327
|
+
|
|
328
|
+
usage = data.get("usage", {})
|
|
329
|
+
|
|
330
|
+
return LLMResponse(
|
|
331
|
+
success=True,
|
|
332
|
+
content=content,
|
|
333
|
+
model=data.get("model", model),
|
|
334
|
+
provider=self.provider_name,
|
|
335
|
+
usage={
|
|
336
|
+
"prompt_tokens": usage.get("prompt_tokens", 0),
|
|
337
|
+
"completion_tokens": usage.get("completion_tokens", 0),
|
|
338
|
+
"total_tokens": usage.get("total_tokens", 0)
|
|
339
|
+
},
|
|
340
|
+
raw=data
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
except requests.ConnectionError:
|
|
344
|
+
raise LLMProviderError(f"Cannot connect to {self.base_url}")
|
|
345
|
+
except requests.Timeout:
|
|
346
|
+
raise LLMProviderError(f"Request timed out after {self.timeout}s")
|
|
347
|
+
except requests.HTTPError as e:
|
|
348
|
+
error_msg = e.response.text
|
|
349
|
+
try:
|
|
350
|
+
error_data = e.response.json()
|
|
351
|
+
error_msg = error_data.get("error", {}).get("message", error_msg)
|
|
352
|
+
except:
|
|
353
|
+
pass
|
|
354
|
+
raise LLMProviderError(f"OpenAI API error: {error_msg}")
|
|
355
|
+
except Exception as e:
|
|
356
|
+
raise LLMProviderError(f"OpenAI error: {e}")
|
|
357
|
+
|
|
358
|
+
def list_models(self) -> List[str]:
|
|
359
|
+
"""List available models."""
|
|
360
|
+
try:
|
|
361
|
+
url = f"{self.base_url}/models"
|
|
362
|
+
resp = requests.get(url, headers=self._get_headers(), timeout=10)
|
|
363
|
+
resp.raise_for_status()
|
|
364
|
+
data = resp.json()
|
|
365
|
+
return [m.get("id", "") for m in data.get("data", [])]
|
|
366
|
+
except Exception as e:
|
|
367
|
+
logger.error(f"Error listing models: {e}")
|
|
368
|
+
return []
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
class AnthropicProvider(BaseLLMProvider):
|
|
372
|
+
"""
|
|
373
|
+
Anthropic provider for Claude models.
|
|
374
|
+
|
|
375
|
+
Endpoints:
|
|
376
|
+
- /v1/messages (chat)
|
|
377
|
+
|
|
378
|
+
Default URL: https://api.anthropic.com
|
|
379
|
+
"""
|
|
380
|
+
|
|
381
|
+
@property
|
|
382
|
+
def provider_name(self) -> str:
|
|
383
|
+
return "anthropic"
|
|
384
|
+
|
|
385
|
+
def __init__(
|
|
386
|
+
self,
|
|
387
|
+
base_url: str = None,
|
|
388
|
+
api_key: str = None,
|
|
389
|
+
default_model: str = None,
|
|
390
|
+
timeout: int = 60,
|
|
391
|
+
**kwargs
|
|
392
|
+
):
|
|
393
|
+
base_url = base_url or os.getenv('ANTHROPIC_API_BASE', 'https://api.anthropic.com')
|
|
394
|
+
api_key = api_key or os.getenv('ANTHROPIC_API_KEY', '')
|
|
395
|
+
default_model = default_model or os.getenv('ANTHROPIC_MODEL', 'claude-3-haiku-20240307')
|
|
396
|
+
|
|
397
|
+
super().__init__(base_url, api_key, default_model, timeout)
|
|
398
|
+
|
|
399
|
+
def _get_headers(self) -> Dict[str, str]:
|
|
400
|
+
"""Anthropic uses different auth header."""
|
|
401
|
+
return {
|
|
402
|
+
"Content-Type": "application/json",
|
|
403
|
+
"x-api-key": self.api_key or "",
|
|
404
|
+
"anthropic-version": "2023-06-01"
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
def chat(
|
|
408
|
+
self,
|
|
409
|
+
messages: List[Dict[str, str]],
|
|
410
|
+
model: Optional[str] = None,
|
|
411
|
+
temperature: Optional[float] = None,
|
|
412
|
+
max_tokens: Optional[int] = None,
|
|
413
|
+
**kwargs
|
|
414
|
+
) -> LLMResponse:
|
|
415
|
+
"""Send chat request to Anthropic API."""
|
|
416
|
+
model = model or self.default_model
|
|
417
|
+
|
|
418
|
+
# Anthropic requires system message to be separate
|
|
419
|
+
system_content = ""
|
|
420
|
+
chat_messages = []
|
|
421
|
+
|
|
422
|
+
for msg in messages:
|
|
423
|
+
if msg.get("role") == "system":
|
|
424
|
+
system_content += msg.get("content", "") + "\n"
|
|
425
|
+
else:
|
|
426
|
+
chat_messages.append({
|
|
427
|
+
"role": msg.get("role", "user"),
|
|
428
|
+
"content": msg.get("content", "")
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
payload = {
|
|
432
|
+
"model": model,
|
|
433
|
+
"messages": chat_messages,
|
|
434
|
+
"max_tokens": max_tokens or 4096,
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if system_content:
|
|
438
|
+
payload["system"] = system_content.strip()
|
|
439
|
+
|
|
440
|
+
if temperature is not None:
|
|
441
|
+
payload["temperature"] = temperature
|
|
442
|
+
|
|
443
|
+
try:
|
|
444
|
+
url = f"{self.base_url}/v1/messages"
|
|
445
|
+
logger.debug(f"Anthropic chat: model={model}")
|
|
446
|
+
|
|
447
|
+
resp = requests.post(
|
|
448
|
+
url,
|
|
449
|
+
json=payload,
|
|
450
|
+
headers=self._get_headers(),
|
|
451
|
+
timeout=self.timeout
|
|
452
|
+
)
|
|
453
|
+
resp.raise_for_status()
|
|
454
|
+
|
|
455
|
+
data = resp.json()
|
|
456
|
+
|
|
457
|
+
# Extract content from Anthropic response format
|
|
458
|
+
content_blocks = data.get("content", [])
|
|
459
|
+
content = ""
|
|
460
|
+
for block in content_blocks:
|
|
461
|
+
if block.get("type") == "text":
|
|
462
|
+
content += block.get("text", "")
|
|
463
|
+
|
|
464
|
+
usage = data.get("usage", {})
|
|
465
|
+
|
|
466
|
+
return LLMResponse(
|
|
467
|
+
success=True,
|
|
468
|
+
content=content,
|
|
469
|
+
model=data.get("model", model),
|
|
470
|
+
provider=self.provider_name,
|
|
471
|
+
usage={
|
|
472
|
+
"prompt_tokens": usage.get("input_tokens", 0),
|
|
473
|
+
"completion_tokens": usage.get("output_tokens", 0),
|
|
474
|
+
"total_tokens": usage.get("input_tokens", 0) + usage.get("output_tokens", 0)
|
|
475
|
+
},
|
|
476
|
+
raw=data
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
except requests.ConnectionError:
|
|
480
|
+
raise LLMProviderError(f"Cannot connect to Anthropic API at {self.base_url}")
|
|
481
|
+
except requests.Timeout:
|
|
482
|
+
raise LLMProviderError(f"Request timed out after {self.timeout}s")
|
|
483
|
+
except requests.HTTPError as e:
|
|
484
|
+
error_msg = e.response.text
|
|
485
|
+
try:
|
|
486
|
+
error_data = e.response.json()
|
|
487
|
+
error_msg = error_data.get("error", {}).get("message", error_msg)
|
|
488
|
+
except:
|
|
489
|
+
pass
|
|
490
|
+
raise LLMProviderError(f"Anthropic API error: {error_msg}")
|
|
491
|
+
except Exception as e:
|
|
492
|
+
raise LLMProviderError(f"Anthropic error: {e}")
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
# Provider Registry
|
|
496
|
+
_PROVIDERS = {
|
|
497
|
+
"ollama": OllamaProvider,
|
|
498
|
+
"openai": OpenAIProvider,
|
|
499
|
+
"lmstudio": OpenAIProvider, # LM Studio uses OpenAI-compatible API
|
|
500
|
+
"anthropic": AnthropicProvider,
|
|
501
|
+
"claude": AnthropicProvider,
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def detect_provider(endpoint: str) -> str:
|
|
506
|
+
"""
|
|
507
|
+
Auto-detect provider from endpoint URL.
|
|
508
|
+
|
|
509
|
+
Args:
|
|
510
|
+
endpoint: The API endpoint URL
|
|
511
|
+
|
|
512
|
+
Returns:
|
|
513
|
+
Provider name (ollama, openai, anthropic)
|
|
514
|
+
"""
|
|
515
|
+
endpoint = endpoint.lower()
|
|
516
|
+
|
|
517
|
+
# Anthropic
|
|
518
|
+
if "anthropic" in endpoint:
|
|
519
|
+
return "anthropic"
|
|
520
|
+
|
|
521
|
+
# OpenAI
|
|
522
|
+
if "openai" in endpoint or "api.openai.com" in endpoint:
|
|
523
|
+
return "openai"
|
|
524
|
+
|
|
525
|
+
# Check for OpenAI-compatible endpoints (LM Studio, etc.)
|
|
526
|
+
if "/v1/" in endpoint or endpoint.endswith("/v1"):
|
|
527
|
+
return "openai"
|
|
528
|
+
|
|
529
|
+
# Default to Ollama for local endpoints
|
|
530
|
+
if "localhost" in endpoint or "127.0.0.1" in endpoint:
|
|
531
|
+
if "11434" in endpoint:
|
|
532
|
+
return "ollama"
|
|
533
|
+
if "1234" in endpoint:
|
|
534
|
+
return "openai" # LM Studio default port
|
|
535
|
+
|
|
536
|
+
# Default
|
|
537
|
+
return "ollama"
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def get_llm_provider(
|
|
541
|
+
provider: str = "auto",
|
|
542
|
+
endpoint: Optional[str] = None,
|
|
543
|
+
api_key: Optional[str] = None,
|
|
544
|
+
model: Optional[str] = None,
|
|
545
|
+
timeout: int = 60
|
|
546
|
+
) -> BaseLLMProvider:
|
|
547
|
+
"""
|
|
548
|
+
Get an LLM provider instance.
|
|
549
|
+
|
|
550
|
+
Args:
|
|
551
|
+
provider: Provider name (ollama, openai, anthropic, auto)
|
|
552
|
+
endpoint: API endpoint URL
|
|
553
|
+
api_key: API key for cloud providers
|
|
554
|
+
model: Default model name
|
|
555
|
+
timeout: Request timeout in seconds
|
|
556
|
+
|
|
557
|
+
Returns:
|
|
558
|
+
BaseLLMProvider instance
|
|
559
|
+
|
|
560
|
+
Examples:
|
|
561
|
+
# Auto-detect from endpoint
|
|
562
|
+
p = get_llm_provider(endpoint="http://localhost:11434") # Ollama
|
|
563
|
+
|
|
564
|
+
# Explicit provider
|
|
565
|
+
p = get_llm_provider(provider="openai", api_key="sk-...")
|
|
566
|
+
|
|
567
|
+
# LM Studio (uses OpenAI-compatible API)
|
|
568
|
+
p = get_llm_provider(endpoint="http://localhost:1234/v1")
|
|
569
|
+
"""
|
|
570
|
+
# Auto-detect provider from endpoint if not specified
|
|
571
|
+
if provider == "auto" and endpoint:
|
|
572
|
+
provider = detect_provider(endpoint)
|
|
573
|
+
|
|
574
|
+
# Get provider class
|
|
575
|
+
provider_cls = _PROVIDERS.get(provider.lower())
|
|
576
|
+
if not provider_cls:
|
|
577
|
+
raise ValueError(f"Unknown provider: {provider}. Valid: {list(_PROVIDERS.keys())}")
|
|
578
|
+
|
|
579
|
+
# Create instance
|
|
580
|
+
kwargs = {"timeout": timeout}
|
|
581
|
+
if endpoint:
|
|
582
|
+
kwargs["base_url"] = endpoint
|
|
583
|
+
if api_key:
|
|
584
|
+
kwargs["api_key"] = api_key
|
|
585
|
+
if model:
|
|
586
|
+
kwargs["default_model"] = model
|
|
587
|
+
|
|
588
|
+
return provider_cls(**kwargs)
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
# Convenience function for backward compatibility with LLMService
|
|
592
|
+
class MultiProviderLLMService:
|
|
593
|
+
"""
|
|
594
|
+
Drop-in replacement for LLMService with multi-provider support.
|
|
595
|
+
|
|
596
|
+
Usage:
|
|
597
|
+
service = MultiProviderLLMService()
|
|
598
|
+
|
|
599
|
+
# Uses Ollama by default
|
|
600
|
+
result = service.chat(messages=[...])
|
|
601
|
+
|
|
602
|
+
# Use specific provider via endpoint
|
|
603
|
+
result = service.chat(messages=[...], endpoint="http://localhost:1234/v1")
|
|
604
|
+
|
|
605
|
+
# Use cloud provider
|
|
606
|
+
result = service.chat(messages=[...], provider="openai", api_key="sk-...")
|
|
607
|
+
"""
|
|
608
|
+
|
|
609
|
+
def __init__(
|
|
610
|
+
self,
|
|
611
|
+
default_provider: str = "ollama",
|
|
612
|
+
default_endpoint: Optional[str] = None,
|
|
613
|
+
default_api_key: Optional[str] = None,
|
|
614
|
+
default_model: Optional[str] = None,
|
|
615
|
+
timeout: int = 60
|
|
616
|
+
):
|
|
617
|
+
self.default_provider = default_provider
|
|
618
|
+
self.default_endpoint = default_endpoint
|
|
619
|
+
self.default_api_key = default_api_key
|
|
620
|
+
self.default_model = default_model
|
|
621
|
+
self.timeout = timeout
|
|
622
|
+
|
|
623
|
+
# Cache providers
|
|
624
|
+
self._providers: Dict[str, BaseLLMProvider] = {}
|
|
625
|
+
|
|
626
|
+
def _get_provider(
|
|
627
|
+
self,
|
|
628
|
+
provider: Optional[str] = None,
|
|
629
|
+
endpoint: Optional[str] = None,
|
|
630
|
+
api_key: Optional[str] = None
|
|
631
|
+
) -> BaseLLMProvider:
|
|
632
|
+
"""Get or create provider instance."""
|
|
633
|
+
# Build cache key
|
|
634
|
+
provider = provider or self.default_provider
|
|
635
|
+
endpoint = endpoint or self.default_endpoint
|
|
636
|
+
api_key = api_key or self.default_api_key
|
|
637
|
+
|
|
638
|
+
cache_key = f"{provider}:{endpoint or 'default'}"
|
|
639
|
+
|
|
640
|
+
if cache_key not in self._providers:
|
|
641
|
+
self._providers[cache_key] = get_llm_provider(
|
|
642
|
+
provider=provider,
|
|
643
|
+
endpoint=endpoint,
|
|
644
|
+
api_key=api_key,
|
|
645
|
+
model=self.default_model,
|
|
646
|
+
timeout=self.timeout
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
return self._providers[cache_key]
|
|
650
|
+
|
|
651
|
+
def chat(
|
|
652
|
+
self,
|
|
653
|
+
messages: List[Dict[str, str]],
|
|
654
|
+
model: Optional[str] = None,
|
|
655
|
+
provider: Optional[str] = None,
|
|
656
|
+
endpoint: Optional[str] = None,
|
|
657
|
+
api_key: Optional[str] = None,
|
|
658
|
+
temperature: Optional[float] = None,
|
|
659
|
+
max_tokens: Optional[int] = None,
|
|
660
|
+
response_format: Optional[str] = None,
|
|
661
|
+
**kwargs
|
|
662
|
+
) -> Dict[str, Any]:
|
|
663
|
+
"""
|
|
664
|
+
Send chat request to any provider.
|
|
665
|
+
|
|
666
|
+
Returns dict compatible with existing LLMService.
|
|
667
|
+
"""
|
|
668
|
+
llm = self._get_provider(provider, endpoint, api_key)
|
|
669
|
+
result = llm.chat(
|
|
670
|
+
messages=messages,
|
|
671
|
+
model=model,
|
|
672
|
+
temperature=temperature,
|
|
673
|
+
max_tokens=max_tokens,
|
|
674
|
+
response_format=response_format,
|
|
675
|
+
**kwargs
|
|
676
|
+
)
|
|
677
|
+
return result.to_dict()
|
|
678
|
+
|
|
679
|
+
def generate(
|
|
680
|
+
self,
|
|
681
|
+
prompt: str,
|
|
682
|
+
model: Optional[str] = None,
|
|
683
|
+
system: Optional[str] = None,
|
|
684
|
+
provider: Optional[str] = None,
|
|
685
|
+
endpoint: Optional[str] = None,
|
|
686
|
+
api_key: Optional[str] = None,
|
|
687
|
+
**kwargs
|
|
688
|
+
) -> Dict[str, Any]:
|
|
689
|
+
"""Generate completion from prompt."""
|
|
690
|
+
llm = self._get_provider(provider, endpoint, api_key)
|
|
691
|
+
result = llm.generate(prompt=prompt, model=model, system=system, **kwargs)
|
|
692
|
+
return result.to_dict()
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
# Global instance
|
|
696
|
+
_multi_llm_service: Optional[MultiProviderLLMService] = None
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
def get_multi_llm_service() -> MultiProviderLLMService:
|
|
700
|
+
"""Get global multi-provider LLM service."""
|
|
701
|
+
global _multi_llm_service
|
|
702
|
+
if _multi_llm_service is None:
|
|
703
|
+
_multi_llm_service = MultiProviderLLMService()
|
|
704
|
+
return _multi_llm_service
|