init
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
import httpx
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.config import settings
|
||||
from app.models.app_settings import AppSettings
|
||||
|
||||
logger = logging.getLogger("app.services.ai_advisor")
|
||||
|
||||
@@ -34,71 +37,133 @@ SYSTEM_PROMPT = """
|
||||
"""
|
||||
|
||||
|
||||
async def _get_setting(db: AsyncSession, key: str, default: str = "") -> str:
|
||||
result = await db.execute(select(AppSettings.value).where(AppSettings.key == key))
|
||||
row = result.scalar_one_or_none()
|
||||
return row if row is not None else default
|
||||
|
||||
|
||||
def _make_proxy_signature(salt: str) -> tuple[str, int]:
|
||||
timestamp = int(time.time())
|
||||
hash_input = f"{timestamp}{salt}".encode()
|
||||
signature = hashlib.sha256(hash_input).hexdigest()
|
||||
return signature, timestamp
|
||||
|
||||
|
||||
async def _call_via_proxy(proxy_url: str, proxy_salt: str, messages: list[dict]) -> str:
|
||||
"""Send request through AI proxy."""
|
||||
signature, timestamp = _make_proxy_signature(proxy_salt)
|
||||
headers = {
|
||||
"x-signature": signature,
|
||||
"x-timestamp": str(timestamp),
|
||||
}
|
||||
async with httpx.AsyncClient(timeout=60.0) as client:
|
||||
resp = await client.post(
|
||||
f"{proxy_url.rstrip('/')}/generate_text",
|
||||
json={"messages": messages},
|
||||
headers=headers,
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
logger.error("AI proxy error %d: %s", resp.status_code, resp.text[:500])
|
||||
raise ValueError(f"AI proxy вернул ошибку: {resp.status_code}")
|
||||
|
||||
data = resp.json()
|
||||
text = data.get("response", "")
|
||||
if not text:
|
||||
raise ValueError(f"AI proxy вернул пустой ответ (finish_reason={data.get('finish_reason')})")
|
||||
return text
|
||||
|
||||
|
||||
async def _call_direct(api_key: str, system: str, user_message: str) -> str:
|
||||
"""Call Google GenAI directly via REST (no SDK dependency)."""
|
||||
url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={api_key}"
|
||||
payload = {
|
||||
"system_instruction": {"parts": [{"text": system}]},
|
||||
"contents": [{"role": "user", "parts": [{"text": user_message}]}],
|
||||
"generationConfig": {"temperature": 0.3, "maxOutputTokens": 1024},
|
||||
}
|
||||
async with httpx.AsyncClient(timeout=60.0) as client:
|
||||
resp = await client.post(url, json=payload)
|
||||
if resp.status_code != 200:
|
||||
logger.error("Google API error %d: %s", resp.status_code, resp.text[:500])
|
||||
raise ValueError(f"Google API ошибка: {resp.status_code}")
|
||||
|
||||
data = resp.json()
|
||||
candidates = data.get("candidates", [])
|
||||
if not candidates:
|
||||
raise ValueError("Google API вернул пустой ответ")
|
||||
parts = candidates[0].get("content", {}).get("parts", [])
|
||||
return parts[0].get("text", "") if parts else ""
|
||||
|
||||
|
||||
def _parse_json_response(text: str) -> dict:
|
||||
try:
|
||||
return json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
start = text.find("{")
|
||||
end = text.rfind("}") + 1
|
||||
if start != -1 and end > start:
|
||||
return json.loads(text[start:end])
|
||||
raise ValueError("AI вернул невалидный JSON")
|
||||
|
||||
|
||||
async def get_material_recommendation(
|
||||
task_description: str,
|
||||
materials_data: list[dict],
|
||||
budget_preference: str = "optimal",
|
||||
file_info: dict | None = None,
|
||||
db: AsyncSession | None = None,
|
||||
) -> dict:
|
||||
"""Get material recommendation from Google Gemini API."""
|
||||
"""Get material recommendation — via proxy or direct, based on DB settings."""
|
||||
logger.info("=== AI Advisor request ===")
|
||||
logger.info("Task: %s", task_description)
|
||||
logger.info("Budget preference: %s", budget_preference)
|
||||
logger.info("File info: %s", file_info)
|
||||
logger.info("Materials count: %d", len(materials_data))
|
||||
|
||||
if not settings.GOOGLE_API_KEY:
|
||||
logger.error("GOOGLE_API_KEY is not configured")
|
||||
raise ValueError("GOOGLE_API_KEY not configured")
|
||||
# Read settings from DB, fallback to config
|
||||
use_proxy = True
|
||||
proxy_url = settings.AI_PROXY_URL
|
||||
proxy_salt = settings.AI_PROXY_SALT
|
||||
direct_api_key = settings.GOOGLE_API_KEY
|
||||
|
||||
if db:
|
||||
use_proxy_str = await _get_setting(db, "ai_use_proxy", "true")
|
||||
use_proxy = use_proxy_str.lower() in ("true", "1", "yes")
|
||||
db_proxy_url = await _get_setting(db, "ai_proxy_url")
|
||||
db_proxy_salt = await _get_setting(db, "ai_proxy_salt")
|
||||
db_api_key = await _get_setting(db, "ai_direct_api_key")
|
||||
if db_proxy_url:
|
||||
proxy_url = db_proxy_url
|
||||
if db_proxy_salt:
|
||||
proxy_salt = db_proxy_salt
|
||||
if db_api_key:
|
||||
direct_api_key = db_api_key
|
||||
|
||||
materials_json = json.dumps(materials_data, ensure_ascii=False, indent=2)
|
||||
system = SYSTEM_PROMPT.format(materials_json=materials_json)
|
||||
logger.debug("System prompt length: %d chars", len(system))
|
||||
|
||||
user_message = f"Описание задачи: {task_description}\nПредпочтение по бюджету: {budget_preference}"
|
||||
if file_info:
|
||||
user_message += f"\nИнформация о модели: {json.dumps(file_info, ensure_ascii=False)}"
|
||||
logger.debug("User message: %s", user_message)
|
||||
|
||||
logger.info("Sending request to Gemini API (model: gemini-2.0-flash)...")
|
||||
start_time = time.time()
|
||||
|
||||
client = genai.Client(api_key=settings.GOOGLE_API_KEY)
|
||||
response = await client.aio.models.generate_content(
|
||||
model="gemini-3-flash-preview",
|
||||
contents=user_message,
|
||||
config=types.GenerateContentConfig(
|
||||
system_instruction=system,
|
||||
max_output_tokens=1024,
|
||||
temperature=0.3,
|
||||
),
|
||||
)
|
||||
if use_proxy:
|
||||
logger.info("Mode: PROXY (%s)", proxy_url)
|
||||
if not proxy_url:
|
||||
raise ValueError("AI proxy URL не настроен")
|
||||
messages = [{"role": "user", "content": system + "\n\n" + user_message}]
|
||||
response_text = await _call_via_proxy(proxy_url, proxy_salt, messages)
|
||||
else:
|
||||
logger.info("Mode: DIRECT (Google API)")
|
||||
if not direct_api_key:
|
||||
raise ValueError("Google API Key не настроен")
|
||||
response_text = await _call_direct(direct_api_key, system, user_message)
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
logger.info("Gemini API responded in %.2f seconds", elapsed)
|
||||
|
||||
response_text = response.text
|
||||
logger.info("AI responded in %.2f seconds", elapsed)
|
||||
logger.debug("Raw response (%d chars): %s", len(response_text), response_text[:500])
|
||||
|
||||
try:
|
||||
result = json.loads(response_text)
|
||||
logger.info("Response parsed as JSON successfully")
|
||||
logger.info("Recommended material: id=%s, name=%s",
|
||||
result.get("recommended_material_id"), result.get("recommended_material_name"))
|
||||
logger.info("Alternatives: %d, Questions: %d",
|
||||
len(result.get("alternatives", [])), len(result.get("questions", [])))
|
||||
logger.info("=== AI Advisor complete ===")
|
||||
return result
|
||||
except json.JSONDecodeError:
|
||||
logger.warning("Direct JSON parse failed, trying to extract JSON from response...")
|
||||
start = response_text.find("{")
|
||||
end = response_text.rfind("}") + 1
|
||||
if start != -1 and end > start:
|
||||
extracted = response_text[start:end]
|
||||
logger.debug("Extracted JSON substring [%d:%d]: %s", start, end, extracted[:300])
|
||||
result = json.loads(extracted)
|
||||
logger.info("Extracted JSON parsed successfully")
|
||||
logger.info("=== AI Advisor complete ===")
|
||||
return result
|
||||
logger.error("Failed to extract JSON from AI response: %s", response_text[:200])
|
||||
raise ValueError("AI вернул невалидный JSON")
|
||||
result = _parse_json_response(response_text)
|
||||
logger.info("Recommended: id=%s, name=%s",
|
||||
result.get("recommended_material_id"), result.get("recommended_material_name"))
|
||||
logger.info("=== AI Advisor complete ===")
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user