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.
Files changed (262) hide show
  1. quantum/__init__.py +1 -0
  2. quantum/cli/__init__.py +2 -0
  3. quantum/cli/commands/__init__.py +15 -0
  4. quantum/cli/commands/build.py +417 -0
  5. quantum/cli/commands/dev.py +185 -0
  6. quantum/cli/commands/docs.py +329 -0
  7. quantum/cli/commands/lint.py +523 -0
  8. quantum/cli/commands/migrate.py +527 -0
  9. quantum/cli/commands/new.py +622 -0
  10. quantum/cli/commands/serve.py +190 -0
  11. quantum/cli/commands/test.py +193 -0
  12. quantum/cli/deploy.py +810 -0
  13. quantum/cli/hot_reload.py +951 -0
  14. quantum/cli/jobs.py +356 -0
  15. quantum/cli/mq.py +582 -0
  16. quantum/cli/pkg.py +390 -0
  17. quantum/cli/runner.py +547 -0
  18. quantum/cli/server_process.py +159 -0
  19. quantum/cli/utils.py +334 -0
  20. quantum/compiler/__init__.py +30 -0
  21. quantum/compiler/base_generator.py +367 -0
  22. quantum/compiler/cli.py +295 -0
  23. quantum/compiler/expression_transformer.py +444 -0
  24. quantum/compiler/javascript/__init__.py +10 -0
  25. quantum/compiler/javascript/generator.py +659 -0
  26. quantum/compiler/optimizer.py +270 -0
  27. quantum/compiler/python/__init__.py +10 -0
  28. quantum/compiler/python/generator.py +883 -0
  29. quantum/compiler/python/runtime.py +863 -0
  30. quantum/compiler/transpiler.py +330 -0
  31. quantum/core/__init__.py +3 -0
  32. quantum/core/ast_nodes.py +2611 -0
  33. quantum/core/expression_diagnostics.py +87 -0
  34. quantum/core/expression_stdlib.py +148 -0
  35. quantum/core/expressions.py +591 -0
  36. quantum/core/features/agents/src/__init__.py +30 -0
  37. quantum/core/features/agents/src/ast_node.py +540 -0
  38. quantum/core/features/conditionals/src/__init__.py +8 -0
  39. quantum/core/features/conditionals/src/ast_node.py +68 -0
  40. quantum/core/features/data_fetching/src/__init__.py +21 -0
  41. quantum/core/features/data_fetching/src/ast_node.py +312 -0
  42. quantum/core/features/data_fetching/src/desktop_adapter.py +351 -0
  43. quantum/core/features/data_fetching/src/html_adapter.py +474 -0
  44. quantum/core/features/data_fetching/src/parser.py +225 -0
  45. quantum/core/features/data_import/src/__init__.py +0 -0
  46. quantum/core/features/data_import/src/ast_node.py +291 -0
  47. quantum/core/features/data_import/src/runtime.py +538 -0
  48. quantum/core/features/dump/src/__init__.py +12 -0
  49. quantum/core/features/dump/src/ast_node.py +106 -0
  50. quantum/core/features/dump/src/parser.py +61 -0
  51. quantum/core/features/dump/src/runtime.py +246 -0
  52. quantum/core/features/functions/src/__init__.py +8 -0
  53. quantum/core/features/functions/src/ast_node.py +149 -0
  54. quantum/core/features/game_engine_2d/src/__init__.py +19 -0
  55. quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
  56. quantum/core/features/game_engine_2d/src/parser.py +983 -0
  57. quantum/core/features/invocation/src/__init__.py +0 -0
  58. quantum/core/features/invocation/src/ast_node.py +146 -0
  59. quantum/core/features/invocation/src/runtime.py +327 -0
  60. quantum/core/features/knowledge_base/src/__init__.py +6 -0
  61. quantum/core/features/knowledge_base/src/ast_node.py +113 -0
  62. quantum/core/features/knowledge_base/src/parser.py +82 -0
  63. quantum/core/features/logging/src/__init__.py +12 -0
  64. quantum/core/features/logging/src/ast_node.py +111 -0
  65. quantum/core/features/logging/src/parser.py +50 -0
  66. quantum/core/features/logging/src/runtime.py +190 -0
  67. quantum/core/features/loops/src/__init__.py +8 -0
  68. quantum/core/features/loops/src/ast_node.py +60 -0
  69. quantum/core/features/query/src/__init__.py +0 -0
  70. quantum/core/features/query/src/database_service.py +322 -0
  71. quantum/core/features/query/src/query_validators.py +20 -0
  72. quantum/core/features/state_management/src/__init__.py +11 -0
  73. quantum/core/features/state_management/src/ast_node.py +228 -0
  74. quantum/core/features/terminal_engine/src/__init__.py +21 -0
  75. quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
  76. quantum/core/features/terminal_engine/src/parser.py +361 -0
  77. quantum/core/features/testing_engine/src/__init__.py +41 -0
  78. quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
  79. quantum/core/features/testing_engine/src/parser.py +604 -0
  80. quantum/core/features/theming/src/__init__.py +48 -0
  81. quantum/core/features/theming/src/ast_node.py +137 -0
  82. quantum/core/features/theming/src/presets.py +405 -0
  83. quantum/core/features/ui_engine/src/__init__.py +20 -0
  84. quantum/core/features/ui_engine/src/ast_nodes.py +1854 -0
  85. quantum/core/features/ui_engine/src/parser.py +1106 -0
  86. quantum/core/features/websocket/src/__init__.py +24 -0
  87. quantum/core/features/websocket/src/ast_node.py +247 -0
  88. quantum/core/html_compat.py +299 -0
  89. quantum/core/parser.py +1235 -0
  90. quantum/core/parser_registry.py +213 -0
  91. quantum/core/parsers/__init__.py +76 -0
  92. quantum/core/parsers/ai/__init__.py +12 -0
  93. quantum/core/parsers/ai/agent_parser.py +106 -0
  94. quantum/core/parsers/ai/knowledge_parser.py +86 -0
  95. quantum/core/parsers/ai/llm_parser.py +78 -0
  96. quantum/core/parsers/ai/team_parser.py +88 -0
  97. quantum/core/parsers/base.py +322 -0
  98. quantum/core/parsers/composition/__init__.py +10 -0
  99. quantum/core/parsers/composition/import_parser.py +50 -0
  100. quantum/core/parsers/composition/slot_parser.py +48 -0
  101. quantum/core/parsers/control_flow/__init__.py +11 -0
  102. quantum/core/parsers/control_flow/if_parser.py +68 -0
  103. quantum/core/parsers/control_flow/loop_parser.py +105 -0
  104. quantum/core/parsers/control_flow/set_parser.py +89 -0
  105. quantum/core/parsers/data/__init__.py +12 -0
  106. quantum/core/parsers/data/data_parser.py +217 -0
  107. quantum/core/parsers/data/invoke_parser.py +116 -0
  108. quantum/core/parsers/data/query_parser.py +188 -0
  109. quantum/core/parsers/data/transaction_parser.py +77 -0
  110. quantum/core/parsers/events/__init__.py +9 -0
  111. quantum/core/parsers/events/dispatch_event_parser.py +44 -0
  112. quantum/core/parsers/forms/__init__.py +11 -0
  113. quantum/core/parsers/forms/action_parser.py +54 -0
  114. quantum/core/parsers/forms/flash_parser.py +34 -0
  115. quantum/core/parsers/forms/redirect_parser.py +33 -0
  116. quantum/core/parsers/functions/__init__.py +11 -0
  117. quantum/core/parsers/functions/function_parser.py +137 -0
  118. quantum/core/parsers/functions/param_parser.py +66 -0
  119. quantum/core/parsers/functions/return_parser.py +31 -0
  120. quantum/core/parsers/html/__init__.py +10 -0
  121. quantum/core/parsers/html/component_call_parser.py +130 -0
  122. quantum/core/parsers/html/html_parser.py +115 -0
  123. quantum/core/parsers/jobs/__init__.py +11 -0
  124. quantum/core/parsers/jobs/job_parser.py +71 -0
  125. quantum/core/parsers/jobs/schedule_parser.py +62 -0
  126. quantum/core/parsers/jobs/thread_parser.py +57 -0
  127. quantum/core/parsers/messaging/__init__.py +17 -0
  128. quantum/core/parsers/messaging/message_ack_parser.py +30 -0
  129. quantum/core/parsers/messaging/message_nack_parser.py +30 -0
  130. quantum/core/parsers/messaging/message_parser.py +114 -0
  131. quantum/core/parsers/messaging/queue_parser.py +61 -0
  132. quantum/core/parsers/messaging/websocket_parser.py +121 -0
  133. quantum/core/parsers/persistence/__init__.py +9 -0
  134. quantum/core/parsers/persistence/persist_parser.py +64 -0
  135. quantum/core/parsers/routing/__init__.py +9 -0
  136. quantum/core/parsers/routing/route_parser.py +41 -0
  137. quantum/core/parsers/scripting/__init__.py +12 -0
  138. quantum/core/parsers/scripting/pyclass_parser.py +62 -0
  139. quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
  140. quantum/core/parsers/scripting/pyimport_parser.py +52 -0
  141. quantum/core/parsers/scripting/python_parser.py +49 -0
  142. quantum/core/parsers/services/__init__.py +12 -0
  143. quantum/core/parsers/services/dump_parser.py +52 -0
  144. quantum/core/parsers/services/file_parser.py +48 -0
  145. quantum/core/parsers/services/log_parser.py +46 -0
  146. quantum/core/parsers/services/mail_parser.py +65 -0
  147. quantum/core/tiers.py +82 -0
  148. quantum/packages/__init__.py +28 -0
  149. quantum/packages/manager.py +413 -0
  150. quantum/packages/manifest.py +351 -0
  151. quantum/packages/registry.py +399 -0
  152. quantum/packages/resolver.py +336 -0
  153. quantum/plugins/__init__.py +33 -0
  154. quantum/plugins/hooks.py +329 -0
  155. quantum/plugins/loader.py +479 -0
  156. quantum/plugins/manifest.py +336 -0
  157. quantum/plugins/registry.py +371 -0
  158. quantum/runtime/__init__.py +28 -0
  159. quantum/runtime/action_handler.py +443 -0
  160. quantum/runtime/adapters/__init__.py +88 -0
  161. quantum/runtime/adapters/memory_adapter.py +690 -0
  162. quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
  163. quantum/runtime/adapters/redis_adapter.py +582 -0
  164. quantum/runtime/adapters/sqlite_adapter.py +414 -0
  165. quantum/runtime/agent_service.py +1133 -0
  166. quantum/runtime/api_server.py +86 -0
  167. quantum/runtime/ast_cache.py +506 -0
  168. quantum/runtime/auth_service.py +267 -0
  169. quantum/runtime/component.py +990 -0
  170. quantum/runtime/component_composer.py +319 -0
  171. quantum/runtime/component_resolver.py +174 -0
  172. quantum/runtime/database_service.py +598 -0
  173. quantum/runtime/email_service.py +162 -0
  174. quantum/runtime/error_handler.py +295 -0
  175. quantum/runtime/execution_context.py +286 -0
  176. quantum/runtime/executor_registry.py +171 -0
  177. quantum/runtime/executors/__init__.py +71 -0
  178. quantum/runtime/executors/ai/__init__.py +12 -0
  179. quantum/runtime/executors/ai/agent_executor.py +217 -0
  180. quantum/runtime/executors/ai/knowledge_executor.py +114 -0
  181. quantum/runtime/executors/ai/llm_executor.py +153 -0
  182. quantum/runtime/executors/ai/team_executor.py +171 -0
  183. quantum/runtime/executors/base.py +262 -0
  184. quantum/runtime/executors/control_flow/__init__.py +11 -0
  185. quantum/runtime/executors/control_flow/if_executor.py +93 -0
  186. quantum/runtime/executors/control_flow/loop_executor.py +307 -0
  187. quantum/runtime/executors/control_flow/set_executor.py +412 -0
  188. quantum/runtime/executors/data/__init__.py +12 -0
  189. quantum/runtime/executors/data/data_executor.py +145 -0
  190. quantum/runtime/executors/data/invoke_executor.py +176 -0
  191. quantum/runtime/executors/data/query_executor.py +256 -0
  192. quantum/runtime/executors/data/transaction_executor.py +91 -0
  193. quantum/runtime/executors/jobs/__init__.py +11 -0
  194. quantum/runtime/executors/jobs/job_executor.py +190 -0
  195. quantum/runtime/executors/jobs/schedule_executor.py +132 -0
  196. quantum/runtime/executors/jobs/thread_executor.py +127 -0
  197. quantum/runtime/executors/messaging/__init__.py +17 -0
  198. quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
  199. quantum/runtime/executors/messaging/message_executor.py +174 -0
  200. quantum/runtime/executors/messaging/queue_executor.py +103 -0
  201. quantum/runtime/executors/messaging/websocket_executor.py +197 -0
  202. quantum/runtime/executors/scripting/__init__.py +11 -0
  203. quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
  204. quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
  205. quantum/runtime/executors/scripting/python_executor.py +249 -0
  206. quantum/runtime/executors/services/__init__.py +12 -0
  207. quantum/runtime/executors/services/dump_executor.py +72 -0
  208. quantum/runtime/executors/services/file_executor.py +89 -0
  209. quantum/runtime/executors/services/log_executor.py +77 -0
  210. quantum/runtime/executors/services/mail_executor.py +81 -0
  211. quantum/runtime/expression_cache.py +498 -0
  212. quantum/runtime/file_upload_service.py +326 -0
  213. quantum/runtime/function_registry.py +118 -0
  214. quantum/runtime/game_builder.py +166 -0
  215. quantum/runtime/game_code_generator.py +2371 -0
  216. quantum/runtime/game_templates.py +2006 -0
  217. quantum/runtime/godot_code_generator.py +4681 -0
  218. quantum/runtime/godot_templates.py +1449 -0
  219. quantum/runtime/job_executor.py +1599 -0
  220. quantum/runtime/knowledge_service.py +500 -0
  221. quantum/runtime/llm_cache.py +100 -0
  222. quantum/runtime/llm_providers.py +704 -0
  223. quantum/runtime/llm_service.py +287 -0
  224. quantum/runtime/logging_setup.py +140 -0
  225. quantum/runtime/message_broker.py +364 -0
  226. quantum/runtime/message_queue_service.py +571 -0
  227. quantum/runtime/param_validation.py +184 -0
  228. quantum/runtime/pypy_compat.py +315 -0
  229. quantum/runtime/python_bridge.py +698 -0
  230. quantum/runtime/query_validators.py +304 -0
  231. quantum/runtime/renderer.py +733 -0
  232. quantum/runtime/service_container.py +444 -0
  233. quantum/runtime/terminal_builder.py +76 -0
  234. quantum/runtime/terminal_code_generator.py +607 -0
  235. quantum/runtime/terminal_templates.py +243 -0
  236. quantum/runtime/testing_builder.py +77 -0
  237. quantum/runtime/testing_code_generator.py +833 -0
  238. quantum/runtime/testing_templates.py +85 -0
  239. quantum/runtime/ui_builder.py +188 -0
  240. quantum/runtime/ui_desktop_adapter.py +1730 -0
  241. quantum/runtime/ui_desktop_templates.py +307 -0
  242. quantum/runtime/ui_html_adapter.py +2691 -0
  243. quantum/runtime/ui_html_templates.py +2297 -0
  244. quantum/runtime/ui_mobile_adapter.py +1832 -0
  245. quantum/runtime/ui_mobile_templates.py +1003 -0
  246. quantum/runtime/ui_textual_adapter.py +1866 -0
  247. quantum/runtime/ui_textual_templates.py +45 -0
  248. quantum/runtime/ui_tokens.py +465 -0
  249. quantum/runtime/ui_validator.py +365 -0
  250. quantum/runtime/validators.py +256 -0
  251. quantum/runtime/web_server.py +1766 -0
  252. quantum/runtime/websocket_adapter.py +501 -0
  253. quantum/runtime/websocket_service.py +585 -0
  254. quantum/runtime/websocket_transport.py +289 -0
  255. quantum/runtime/wsgi.py +101 -0
  256. quantum/utils/__init__.py +1 -0
  257. quantum_framework-0.9.0.dist-info/METADATA +244 -0
  258. quantum_framework-0.9.0.dist-info/RECORD +262 -0
  259. quantum_framework-0.9.0.dist-info/WHEEL +5 -0
  260. quantum_framework-0.9.0.dist-info/entry_points.txt +2 -0
  261. quantum_framework-0.9.0.dist-info/licenses/LICENSE +21 -0
  262. 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