feat: Initial commit - DAGI Stack v0.2.0 (Phase 2 Complete)
- Router Core with rule-based routing (1530 lines) - DevTools Backend (file ops, test execution) (393 lines) - CrewAI Orchestrator (4 workflows, 12 agents) (358 lines) - Bot Gateway (Telegram/Discord) (321 lines) - RBAC Service (role resolution) (272 lines) - Structured logging (utils/logger.py) - Docker deployment (docker-compose.yml) - Comprehensive documentation (57KB) - Test suites (41 tests, 95% coverage) - Phase 4 roadmap & ecosystem integration plans Production-ready infrastructure for DAARION microDAOs.
This commit is contained in:
168
providers/llm_provider.py
Normal file
168
providers/llm_provider.py
Normal file
@@ -0,0 +1,168 @@
|
||||
"""
|
||||
LLM Provider - supports OpenAI-compatible APIs (Ollama, DeepSeek, etc)
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, Optional
|
||||
|
||||
import httpx
|
||||
|
||||
from router_models import RouterRequest, RouterResponse
|
||||
from .base import Provider
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LLMProvider(Provider):
|
||||
"""
|
||||
LLM Provider using OpenAI-compatible API
|
||||
Works with Ollama, DeepSeek, OpenAI, and other compatible services
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
provider_id: str,
|
||||
base_url: str,
|
||||
model: str,
|
||||
api_key: Optional[str] = None,
|
||||
timeout_s: int = 30,
|
||||
max_tokens: int = 1024,
|
||||
temperature: float = 0.2,
|
||||
provider_type: str = "openai", # "openai" or "ollama"
|
||||
):
|
||||
super().__init__(provider_id)
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.model = model
|
||||
self.api_key = api_key
|
||||
self.timeout_s = timeout_s
|
||||
self.max_tokens = max_tokens
|
||||
self.temperature = temperature
|
||||
self.provider_type = provider_type
|
||||
|
||||
async def call(self, req: RouterRequest) -> RouterResponse:
|
||||
"""Call LLM API"""
|
||||
|
||||
# Extract message from request
|
||||
message = req.message or req.payload.get("message", "")
|
||||
if not message:
|
||||
return RouterResponse(
|
||||
ok=False,
|
||||
provider_id=self.id,
|
||||
error="No message provided"
|
||||
)
|
||||
|
||||
# Build system prompt if agent specified
|
||||
system_prompt = self._get_system_prompt(req)
|
||||
|
||||
# Prepare messages
|
||||
messages = []
|
||||
if system_prompt:
|
||||
messages.append({"role": "system", "content": system_prompt})
|
||||
messages.append({"role": "user", "content": message})
|
||||
|
||||
# Prepare headers
|
||||
headers: Dict[str, str] = {"Content-Type": "application/json"}
|
||||
if self.api_key:
|
||||
headers["Authorization"] = f"Bearer {self.api_key}"
|
||||
|
||||
# Determine endpoint and body based on provider type
|
||||
if self.provider_type == "ollama" or "ollama" in self.base_url.lower():
|
||||
# Ollama uses /v1/chat/completions or /api/chat
|
||||
endpoint = f"{self.base_url}/v1/chat/completions"
|
||||
body = {
|
||||
"model": self.model,
|
||||
"messages": messages,
|
||||
"stream": False,
|
||||
}
|
||||
else:
|
||||
# Standard OpenAI-compatible
|
||||
endpoint = f"{self.base_url}/chat/completions"
|
||||
body = {
|
||||
"model": self.model,
|
||||
"messages": messages,
|
||||
"temperature": self.temperature,
|
||||
"max_tokens": self.max_tokens,
|
||||
}
|
||||
|
||||
# Make request
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=self.timeout_s) as client:
|
||||
logger.info(f"[{self.id}] Calling {endpoint} with model {self.model}")
|
||||
|
||||
response = await client.post(
|
||||
endpoint,
|
||||
json=body,
|
||||
headers=headers,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
except httpx.TimeoutException:
|
||||
logger.error(f"[{self.id}] Request timeout after {self.timeout_s}s")
|
||||
return RouterResponse(
|
||||
ok=False,
|
||||
provider_id=self.id,
|
||||
error=f"Request timeout after {self.timeout_s}s"
|
||||
)
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(f"[{self.id}] HTTP error: {e}")
|
||||
error_detail = e.response.text[:200] if e.response.text else str(e)
|
||||
return RouterResponse(
|
||||
ok=False,
|
||||
provider_id=self.id,
|
||||
error=f"HTTP {e.response.status_code}: {error_detail}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"[{self.id}] Unexpected error: {e}")
|
||||
return RouterResponse(
|
||||
ok=False,
|
||||
provider_id=self.id,
|
||||
error=f"Provider error: {str(e)}"
|
||||
)
|
||||
|
||||
# Parse response
|
||||
try:
|
||||
data = response.json()
|
||||
content = (
|
||||
data.get("choices", [{}])[0]
|
||||
.get("message", {})
|
||||
.get("content", "")
|
||||
)
|
||||
|
||||
usage = data.get("usage", {})
|
||||
|
||||
logger.info(f"[{self.id}] Success. Tokens: {usage.get('total_tokens', 'unknown')}")
|
||||
|
||||
return RouterResponse(
|
||||
ok=True,
|
||||
provider_id=self.id,
|
||||
data={
|
||||
"text": content,
|
||||
"model": self.model,
|
||||
"usage": usage,
|
||||
},
|
||||
metadata={
|
||||
"provider_type": "llm",
|
||||
"model": self.model,
|
||||
"base_url": self.base_url,
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[{self.id}] Failed to parse response: {e}")
|
||||
return RouterResponse(
|
||||
ok=False,
|
||||
provider_id=self.id,
|
||||
error=f"Failed to parse LLM response: {str(e)}"
|
||||
)
|
||||
|
||||
def _get_system_prompt(self, req: RouterRequest) -> Optional[str]:
|
||||
"""Get system prompt based on agent"""
|
||||
# This can be enhanced to load from config
|
||||
if req.agent == "devtools":
|
||||
return (
|
||||
"Ти - DevTools Agent в екосистемі DAARION.city. "
|
||||
"Ти допомагаєш розробникам з аналізом коду, пошуком багів, "
|
||||
"рефакторингом та написанням тестів. "
|
||||
"Відповідай коротко, конкретно, з прикладами коду коли потрібно."
|
||||
)
|
||||
return None
|
||||
Reference in New Issue
Block a user