Files
microdao-daarion/providers/llm_provider.py

205 lines
8.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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 = 60,
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 or context"""
# 1. Check if context.system_prompt provided (e.g., from Gateway)
logger.info(f"[DEBUG] _get_system_prompt called for agent={req.agent}")
logger.info(f"[DEBUG] req.payload type: {type(req.payload)}, keys: {list(req.payload.keys()) if req.payload else []}")
context = req.payload.get("context") or {}
logger.info(f"[DEBUG] context type: {type(context)}, keys: {list(context.keys()) if isinstance(context, dict) else 'not a dict'}")
if isinstance(context, dict) and "system_prompt" in context:
prompt = context["system_prompt"]
logger.info(f"[DEBUG] ✅ Using context.system_prompt: {len(prompt)} chars, agent={req.agent}")
logger.info(f"[DEBUG] System prompt type: {type(prompt)}")
logger.info(f"[DEBUG] System prompt preview (first 200): {str(prompt)[:200]}...")
logger.info(f"[DEBUG] System prompt full length check: {len(str(prompt))} chars")
# Переконаємось, що це рядок
if not isinstance(prompt, str):
logger.warning(f"[DEBUG] ⚠️ System prompt is not a string! Type: {type(prompt)}, value: {prompt}")
prompt = str(prompt) if prompt else None
return prompt
else:
logger.info(f"[DEBUG] ⚠️ No system_prompt in context for agent={req.agent}")
# 2. Agent-specific system prompts (fallback, якщо не передано в context)
if req.agent == "helion":
return (
"Ти - Helion, AI-агент платформи Energy Union екосистеми DAARION.city. "
"Допомагай користувачам з технологіями EcoMiner/BioMiner, токеномікою та DAO governance. "
"Твої основні функції: консультації з енергетичними технологіями, пояснення токеноміки Energy Union, "
"допомога з onboarding в DAO, відповіді на питання про EcoMiner/BioMiner устаткування. "
"Стиль спілкування: професійний, технічний, але зрозумілий, точний у цифрах та даних."
)
if req.agent == "daarwizz":
return (
"Ти — DAARWIZZ, офіційний AI-агент екосистеми DAARION.city. "
"Допомагай учасникам з microDAO, ролями та процесами. "
"Відповідай коротко, практично, враховуй RBAC контекст користувача."
)
if req.agent == "devtools":
return (
"Ти - DevTools Agent в екосистемі DAARION.city. "
"Ти допомагаєш розробникам з аналізом коду, пошуком багів, "
"рефакторингом та написанням тестів. "
"Відповідай коротко, конкретно, з прикладами коду коли потрібно."
)
return None