ai vein
This commit is contained in:
4
soleprint/artery/veins/ia/.env.example
Normal file
4
soleprint/artery/veins/ia/.env.example
Normal file
@@ -0,0 +1,4 @@
|
||||
AI_API_URL=https://api.openai.com/v1
|
||||
AI_API_KEY=your_api_key_here
|
||||
AI_MODEL=gpt-4o
|
||||
API_PORT=8005
|
||||
1
soleprint/artery/veins/ia/__init__.py
Normal file
1
soleprint/artery/veins/ia/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# IA Vein - AI-powered practice tutor
|
||||
12
soleprint/artery/veins/ia/__main__.py
Normal file
12
soleprint/artery/veins/ia/__main__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""Run IA vein: python -m ia"""
|
||||
|
||||
import uvicorn
|
||||
from .core.config import settings
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(
|
||||
"ia.main:app",
|
||||
host="0.0.0.0",
|
||||
port=settings.api_port,
|
||||
reload=True,
|
||||
)
|
||||
0
soleprint/artery/veins/ia/api/__init__.py
Normal file
0
soleprint/artery/veins/ia/api/__init__.py
Normal file
74
soleprint/artery/veins/ia/api/routes.py
Normal file
74
soleprint/artery/veins/ia/api/routes.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
Generic API routes for IA vein.
|
||||
Provides /health and /chat endpoints.
|
||||
Use-case-specific routes are mounted separately.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Header
|
||||
|
||||
from ..core.client import chat_completion, health_check as client_health_check, AIClientError
|
||||
from ..core.config import settings
|
||||
from ..models.chat import ChatRequest, ChatResponse
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def get_api_key(x_ai_token: str | None = None) -> str:
|
||||
"""Resolve API key from header or config. Shared by all routes."""
|
||||
if x_ai_token and x_ai_token.strip():
|
||||
return x_ai_token.strip()
|
||||
if settings.ai_api_key:
|
||||
return settings.ai_api_key
|
||||
raise HTTPException(401, "No AI API key configured")
|
||||
|
||||
|
||||
_decoder = json.JSONDecoder()
|
||||
|
||||
|
||||
def parse_json_response(content: str) -> dict | None:
|
||||
"""Extract first valid JSON object from AI response using the JSON parser itself."""
|
||||
for i, ch in enumerate(content):
|
||||
if ch == "{":
|
||||
try:
|
||||
obj, _ = _decoder.raw_decode(content, i)
|
||||
if isinstance(obj, dict):
|
||||
return obj
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
async def health(x_ai_token: Optional[str] = Header(None)):
|
||||
"""Test AI API connection."""
|
||||
try:
|
||||
key = get_api_key(x_ai_token)
|
||||
result = await client_health_check(key)
|
||||
return result
|
||||
except AIClientError as e:
|
||||
raise HTTPException(503, str(e))
|
||||
|
||||
|
||||
@router.post("/chat")
|
||||
async def chat(
|
||||
req: ChatRequest,
|
||||
x_ai_token: Optional[str] = Header(None),
|
||||
):
|
||||
"""Generic chat completion endpoint."""
|
||||
try:
|
||||
key = get_api_key(x_ai_token)
|
||||
messages = [{"role": m.role, "content": m.content} for m in req.messages]
|
||||
content = await chat_completion(
|
||||
messages,
|
||||
api_key=key,
|
||||
temperature=req.temperature,
|
||||
max_tokens=req.max_tokens,
|
||||
)
|
||||
return ChatResponse(content=content, parsed=parse_json_response(content))
|
||||
except AIClientError as e:
|
||||
raise HTTPException(503, str(e))
|
||||
0
soleprint/artery/veins/ia/core/__init__.py
Normal file
0
soleprint/artery/veins/ia/core/__init__.py
Normal file
73
soleprint/artery/veins/ia/core/client.py
Normal file
73
soleprint/artery/veins/ia/core/client.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""
|
||||
AI API client - OpenAI-compatible chat completions via httpx.
|
||||
"""
|
||||
|
||||
import httpx
|
||||
from .config import settings
|
||||
|
||||
|
||||
class AIClientError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
async def chat_completion(
|
||||
messages: list[dict],
|
||||
model: str | None = None,
|
||||
temperature: float = 0.7,
|
||||
max_tokens: int = 1024,
|
||||
api_key: str | None = None,
|
||||
) -> str:
|
||||
"""Send chat completion request to OpenAI-compatible API."""
|
||||
url = f"{settings.ai_api_url}/chat/completions"
|
||||
key = api_key or settings.ai_api_key
|
||||
|
||||
if not key:
|
||||
raise AIClientError("No API key configured")
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
payload = {
|
||||
"model": model or settings.ai_model,
|
||||
"messages": messages,
|
||||
"temperature": temperature,
|
||||
"max_tokens": max_tokens,
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
try:
|
||||
response = await client.post(url, json=payload, headers=headers)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
return data["choices"][0]["message"]["content"]
|
||||
except httpx.HTTPStatusError as e:
|
||||
raise AIClientError(
|
||||
f"API error {e.response.status_code}: {e.response.text}"
|
||||
)
|
||||
except Exception as e:
|
||||
raise AIClientError(f"Request failed: {e}")
|
||||
|
||||
|
||||
async def health_check(api_key: str | None = None) -> dict:
|
||||
"""Test API connection."""
|
||||
url = f"{settings.ai_api_url}/models"
|
||||
key = api_key or settings.ai_api_key
|
||||
|
||||
if not key:
|
||||
raise AIClientError("No API key configured")
|
||||
|
||||
headers = {"Authorization": f"Bearer {key}"}
|
||||
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
try:
|
||||
response = await client.get(url, headers=headers)
|
||||
response.raise_for_status()
|
||||
return {
|
||||
"status": "ok",
|
||||
"provider": settings.ai_api_url,
|
||||
"model": settings.ai_model,
|
||||
}
|
||||
except Exception as e:
|
||||
raise AIClientError(f"Health check failed: {e}")
|
||||
24
soleprint/artery/veins/ia/core/config.py
Normal file
24
soleprint/artery/veins/ia/core/config.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""
|
||||
IA Vein configuration loaded from .env file.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
ENV_FILE = Path(__file__).parent.parent / ".env"
|
||||
|
||||
|
||||
class IAConfig(BaseSettings):
|
||||
ai_api_url: str = "https://api.openai.com/v1"
|
||||
ai_api_key: str = ""
|
||||
ai_model: str = "gpt-4o"
|
||||
api_port: int = 8005
|
||||
|
||||
model_config = {
|
||||
"env_file": ENV_FILE,
|
||||
"env_file_encoding": "utf-8",
|
||||
"extra": "ignore",
|
||||
}
|
||||
|
||||
|
||||
settings = IAConfig()
|
||||
33
soleprint/artery/veins/ia/main.py
Normal file
33
soleprint/artery/veins/ia/main.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""
|
||||
IA Vein - FastAPI app.
|
||||
|
||||
Generic AI vein with use-case-specific routers mounted as sub-routes.
|
||||
"""
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from .api.routes import router as generic_router
|
||||
from .usecases.practice.routes import router as practice_router
|
||||
from .core.config import settings
|
||||
|
||||
app = FastAPI(title="IA Vein", version="0.1.0")
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["https://mcrn.ar", "http://localhost:8000", "http://localhost:8765", "http://127.0.0.1:8000"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Generic: /ia/health, /ia/chat
|
||||
app.include_router(generic_router, prefix="/ia")
|
||||
|
||||
# Use case: /ia/practice/*
|
||||
app.include_router(practice_router, prefix="/ia/practice")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=settings.api_port)
|
||||
0
soleprint/artery/veins/ia/models/__init__.py
Normal file
0
soleprint/artery/veins/ia/models/__init__.py
Normal file
21
soleprint/artery/veins/ia/models/chat.py
Normal file
21
soleprint/artery/veins/ia/models/chat.py
Normal file
@@ -0,0 +1,21 @@
|
||||
"""
|
||||
Generic request/response models for IA vein.
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ChatMessage(BaseModel):
|
||||
role: str # "system" | "user" | "assistant"
|
||||
content: str
|
||||
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
messages: list[ChatMessage]
|
||||
temperature: float = 0.7
|
||||
max_tokens: int = 1024
|
||||
|
||||
|
||||
class ChatResponse(BaseModel):
|
||||
content: str
|
||||
parsed: dict | None = None
|
||||
5
soleprint/artery/veins/ia/requirements.txt
Normal file
5
soleprint/artery/veins/ia/requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
fastapi>=0.104.0
|
||||
uvicorn>=0.24.0
|
||||
pydantic>=2.0.0
|
||||
pydantic-settings>=2.0.0
|
||||
httpx>=0.25.0
|
||||
18
soleprint/artery/veins/ia/run.py
Normal file
18
soleprint/artery/veins/ia/run.py
Normal file
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env python
|
||||
"""Run the IA vein API."""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
import uvicorn
|
||||
from core.config import settings
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(
|
||||
"main:app",
|
||||
host="0.0.0.0",
|
||||
port=settings.api_port,
|
||||
reload=True,
|
||||
)
|
||||
0
soleprint/artery/veins/ia/usecases/__init__.py
Normal file
0
soleprint/artery/veins/ia/usecases/__init__.py
Normal file
43
soleprint/artery/veins/ia/usecases/practice/formatter.py
Normal file
43
soleprint/artery/veins/ia/usecases/practice/formatter.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""
|
||||
Format practice item context data as text for AI prompt assembly.
|
||||
"""
|
||||
|
||||
|
||||
def _fmt_str(label, val):
|
||||
return f"{label}: {val}"
|
||||
|
||||
|
||||
def _fmt_list(label, val):
|
||||
return f"{label}:\n" + "\n".join(f" - {v}" for v in val)
|
||||
|
||||
|
||||
def _fmt_join(label, val):
|
||||
return f"{label}: {', '.join(val)}"
|
||||
|
||||
|
||||
def _fmt_complexity(label, val):
|
||||
return f"{label}: Time {val.get('time', '?')}, Space {val.get('space', '?')}"
|
||||
|
||||
|
||||
FIELDS = [
|
||||
("oneLiner", "Summary", _fmt_str),
|
||||
("howItWorks", "How it works", _fmt_str),
|
||||
("structure", "Structure", _fmt_str),
|
||||
("whenToUse", "When to use", _fmt_list),
|
||||
("participants", "Participants", _fmt_join),
|
||||
("complexity", "Complexity", _fmt_complexity),
|
||||
]
|
||||
|
||||
|
||||
def format_item_context(item: dict) -> str:
|
||||
"""Format algorithm/pattern data as context string for prompts."""
|
||||
lines = [
|
||||
f"Name: {item.get('name', 'Unknown')}",
|
||||
f"Category: {item.get('category', 'Unknown')}",
|
||||
f"Topic: {item.get('topic', 'unknown')}",
|
||||
]
|
||||
for key, label, fmt in FIELDS:
|
||||
val = item.get(key)
|
||||
if val:
|
||||
lines.append(fmt(label, val))
|
||||
return "\n".join(lines)
|
||||
57
soleprint/artery/veins/ia/usecases/practice/models.py
Normal file
57
soleprint/artery/veins/ia/usecases/practice/models.py
Normal file
@@ -0,0 +1,57 @@
|
||||
"""
|
||||
Request models for the practice tutor use case.
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ItemContext(BaseModel):
|
||||
"""Algorithm or pattern data sent from frontend."""
|
||||
id: str
|
||||
name: str
|
||||
category: str
|
||||
topic: str = "algorithms"
|
||||
whenToUse: list[str] = []
|
||||
howItWorks: str = ""
|
||||
structure: str = ""
|
||||
participants: list[str] = []
|
||||
complexity: dict = {}
|
||||
oneLiner: str = ""
|
||||
|
||||
|
||||
class IdentifyRequest(BaseModel):
|
||||
item: ItemContext
|
||||
lang: str = "en"
|
||||
|
||||
|
||||
class ReviewExplanationRequest(BaseModel):
|
||||
item: ItemContext
|
||||
userExplanation: str
|
||||
lang: str = "en"
|
||||
|
||||
|
||||
class ReviewCodeRequest(BaseModel):
|
||||
item: ItemContext
|
||||
userCode: str
|
||||
language: str = "python"
|
||||
referenceCode: str = ""
|
||||
lang: str = "en"
|
||||
|
||||
|
||||
class ReviewStructureRequest(BaseModel):
|
||||
item: ItemContext
|
||||
userExplanation: str
|
||||
lang: str = "en"
|
||||
|
||||
|
||||
class HintRequest(BaseModel):
|
||||
item: ItemContext
|
||||
mode: str = "identify"
|
||||
context: str = ""
|
||||
lang: str = "en"
|
||||
|
||||
|
||||
class SuggestNextRequest(BaseModel):
|
||||
progress: dict
|
||||
items: list[ItemContext]
|
||||
lang: str = "en"
|
||||
88
soleprint/artery/veins/ia/usecases/practice/prompts.py
Normal file
88
soleprint/artery/veins/ia/usecases/practice/prompts.py
Normal file
@@ -0,0 +1,88 @@
|
||||
"""
|
||||
System prompts for the practice tutor use case.
|
||||
Each function returns a system prompt string for a specific drill mode.
|
||||
"""
|
||||
|
||||
|
||||
def identify(lang: str = "en") -> str:
|
||||
if lang == "es":
|
||||
return (
|
||||
"Eres un tutor de algoritmos y patrones de diseno. "
|
||||
"Genera una descripcion de un problema que se resuelve con la tecnica especificada. "
|
||||
"La descripcion debe ser un escenario realista, sin nombrar la tecnica directamente. "
|
||||
'Responde en JSON: {"problem": "...", "hint": "..."}'
|
||||
)
|
||||
return (
|
||||
"You are an algorithm and design pattern tutor. "
|
||||
"Generate a problem description that is solved by the specified technique. "
|
||||
"The description should be a realistic scenario without naming the technique directly. "
|
||||
'Respond in JSON: {"problem": "...", "hint": "..."}'
|
||||
)
|
||||
|
||||
|
||||
def review_explanation(lang: str = "en") -> str:
|
||||
if lang == "es":
|
||||
return (
|
||||
"Eres un tutor evaluando la explicacion de un estudiante sobre un algoritmo o patron. "
|
||||
"Compara con los datos de referencia. Se constructivo pero preciso. "
|
||||
'Responde en JSON: {"score": "good|partial|weak", "feedback": "...", "missing": ["..."]}'
|
||||
)
|
||||
return (
|
||||
"You are a tutor evaluating a student's explanation of an algorithm or pattern. "
|
||||
"Compare against the reference data. Be constructive but precise. "
|
||||
'Respond in JSON: {"score": "good|partial|weak", "feedback": "...", "missing": ["..."]}'
|
||||
)
|
||||
|
||||
|
||||
def review_code(lang: str = "en") -> str:
|
||||
if lang == "es":
|
||||
return (
|
||||
"Eres un tutor de codigo evaluando la implementacion de un estudiante. "
|
||||
"Evalua: correctitud, eficiencia, estilo, y manejo de edge cases. "
|
||||
'Responde en JSON: {"verdict": "correct|partial|incorrect", "feedback": "...", "improvements": ["..."]}'
|
||||
)
|
||||
return (
|
||||
"You are a code tutor evaluating a student's implementation. "
|
||||
"Evaluate: correctness, efficiency, style, and edge case handling. "
|
||||
'Respond in JSON: {"verdict": "correct|partial|incorrect", "feedback": "...", "improvements": ["..."]}'
|
||||
)
|
||||
|
||||
|
||||
def review_structure(lang: str = "en") -> str:
|
||||
if lang == "es":
|
||||
return (
|
||||
"Eres un tutor evaluando la explicacion de un estudiante sobre la estructura de un patron de diseno. "
|
||||
"Compara participantes, relaciones y responsabilidades con la referencia. "
|
||||
'Responde en JSON: {"score": "good|partial|weak", "feedback": "...", "missing": ["..."]}'
|
||||
)
|
||||
return (
|
||||
"You are a tutor evaluating a student's explanation of a design pattern's structure. "
|
||||
"Compare participants, relationships, and responsibilities against the reference. "
|
||||
'Respond in JSON: {"score": "good|partial|weak", "feedback": "...", "missing": ["..."]}'
|
||||
)
|
||||
|
||||
|
||||
def hint(lang: str = "en") -> str:
|
||||
if lang == "es":
|
||||
return (
|
||||
"Eres un tutor. Da una pista sutil sin revelar la respuesta completa. "
|
||||
"Guia al estudiante hacia el enfoque correcto."
|
||||
)
|
||||
return (
|
||||
"You are a tutor. Give a subtle hint without revealing the full answer. "
|
||||
"Guide the student toward the correct approach."
|
||||
)
|
||||
|
||||
|
||||
def suggest_next(lang: str = "en") -> str:
|
||||
if lang == "es":
|
||||
return (
|
||||
"Eres un tutor. Basandote en el progreso del estudiante, sugiere que practicar. "
|
||||
"Prioriza tecnicas debiles. "
|
||||
'Responde en JSON: {"suggestions": [{"id": "...", "reason": "..."}], "encouragement": "..."}'
|
||||
)
|
||||
return (
|
||||
"You are a tutor. Based on student progress, suggest what to practice next. "
|
||||
"Prioritize weak techniques. "
|
||||
'Respond in JSON: {"suggestions": [{"id": "...", "reason": "..."}], "encouragement": "..."}'
|
||||
)
|
||||
138
soleprint/artery/veins/ia/usecases/practice/routes.py
Normal file
138
soleprint/artery/veins/ia/usecases/practice/routes.py
Normal file
@@ -0,0 +1,138 @@
|
||||
"""
|
||||
Practice tutor use case routes.
|
||||
Mounted under /ia/practice/ by main.py.
|
||||
Uses the generic AI client from core.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Header
|
||||
|
||||
from ...core.client import chat_completion, AIClientError
|
||||
from ...api.routes import get_api_key, parse_json_response
|
||||
from ...models.chat import ChatResponse
|
||||
from . import prompts
|
||||
from .models import (
|
||||
IdentifyRequest,
|
||||
ReviewExplanationRequest,
|
||||
ReviewCodeRequest,
|
||||
ReviewStructureRequest,
|
||||
HintRequest,
|
||||
SuggestNextRequest,
|
||||
)
|
||||
from .formatter import format_item_context
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/identify")
|
||||
async def generate_identify_question(
|
||||
req: IdentifyRequest,
|
||||
x_ai_token: Optional[str] = Header(None),
|
||||
):
|
||||
try:
|
||||
key = get_api_key(x_ai_token)
|
||||
ctx = format_item_context(req.item.model_dump())
|
||||
messages = [
|
||||
{"role": "system", "content": prompts.identify(req.lang)},
|
||||
{"role": "user", "content": f"Generate a problem description for:\n\n{ctx}"},
|
||||
]
|
||||
content = await chat_completion(messages, api_key=key, temperature=0.8)
|
||||
return ChatResponse(content=content, parsed=parse_json_response(content))
|
||||
except AIClientError as e:
|
||||
raise HTTPException(503, str(e))
|
||||
|
||||
|
||||
@router.post("/review-explanation")
|
||||
async def review_explanation(
|
||||
req: ReviewExplanationRequest,
|
||||
x_ai_token: Optional[str] = Header(None),
|
||||
):
|
||||
try:
|
||||
key = get_api_key(x_ai_token)
|
||||
ctx = format_item_context(req.item.model_dump())
|
||||
messages = [
|
||||
{"role": "system", "content": prompts.review_explanation(req.lang)},
|
||||
{"role": "user", "content": f"Reference:\n{ctx}\n\nStudent's explanation:\n{req.userExplanation}"},
|
||||
]
|
||||
content = await chat_completion(messages, api_key=key, temperature=0.3)
|
||||
return ChatResponse(content=content, parsed=parse_json_response(content))
|
||||
except AIClientError as e:
|
||||
raise HTTPException(503, str(e))
|
||||
|
||||
|
||||
@router.post("/review-code")
|
||||
async def review_code(
|
||||
req: ReviewCodeRequest,
|
||||
x_ai_token: Optional[str] = Header(None),
|
||||
):
|
||||
try:
|
||||
key = get_api_key(x_ai_token)
|
||||
ctx = format_item_context(req.item.model_dump())
|
||||
ref = f"\n\nReference ({req.language}):\n```\n{req.referenceCode}\n```" if req.referenceCode else ""
|
||||
messages = [
|
||||
{"role": "system", "content": prompts.review_code(req.lang)},
|
||||
{"role": "user", "content": f"Algorithm:\n{ctx}{ref}\n\nStudent's code ({req.language}):\n```\n{req.userCode}\n```"},
|
||||
]
|
||||
content = await chat_completion(messages, api_key=key, temperature=0.3)
|
||||
return ChatResponse(content=content, parsed=parse_json_response(content))
|
||||
except AIClientError as e:
|
||||
raise HTTPException(503, str(e))
|
||||
|
||||
|
||||
@router.post("/review-structure")
|
||||
async def review_structure(
|
||||
req: ReviewStructureRequest,
|
||||
x_ai_token: Optional[str] = Header(None),
|
||||
):
|
||||
try:
|
||||
key = get_api_key(x_ai_token)
|
||||
ctx = format_item_context(req.item.model_dump())
|
||||
messages = [
|
||||
{"role": "system", "content": prompts.review_structure(req.lang)},
|
||||
{"role": "user", "content": f"Reference:\n{ctx}\n\nStudent's explanation:\n{req.userExplanation}"},
|
||||
]
|
||||
content = await chat_completion(messages, api_key=key, temperature=0.3)
|
||||
return ChatResponse(content=content, parsed=parse_json_response(content))
|
||||
except AIClientError as e:
|
||||
raise HTTPException(503, str(e))
|
||||
|
||||
|
||||
@router.post("/hint")
|
||||
async def get_hint(
|
||||
req: HintRequest,
|
||||
x_ai_token: Optional[str] = Header(None),
|
||||
):
|
||||
try:
|
||||
key = get_api_key(x_ai_token)
|
||||
ctx = format_item_context(req.item.model_dump())
|
||||
messages = [
|
||||
{"role": "system", "content": prompts.hint(req.lang)},
|
||||
{"role": "user", "content": f"Mode: {req.mode}\n{ctx}\n\nStuck on: {req.context or 'general approach'}"},
|
||||
]
|
||||
content = await chat_completion(messages, api_key=key, temperature=0.7, max_tokens=256)
|
||||
return ChatResponse(content=content)
|
||||
except AIClientError as e:
|
||||
raise HTTPException(503, str(e))
|
||||
|
||||
|
||||
@router.post("/suggest-next")
|
||||
async def suggest_next(
|
||||
req: SuggestNextRequest,
|
||||
x_ai_token: Optional[str] = Header(None),
|
||||
):
|
||||
try:
|
||||
key = get_api_key(x_ai_token)
|
||||
progress_str = json.dumps(req.progress, indent=2)
|
||||
names = [i.name for i in req.items]
|
||||
messages = [
|
||||
{"role": "system", "content": prompts.suggest_next(req.lang)},
|
||||
{"role": "user", "content": f"Available: {', '.join(names)}\n\nProgress:\n{progress_str}"},
|
||||
]
|
||||
content = await chat_completion(messages, api_key=key, temperature=0.5)
|
||||
return ChatResponse(content=content, parsed=parse_json_response(content))
|
||||
except AIClientError as e:
|
||||
raise HTTPException(503, str(e))
|
||||
Reference in New Issue
Block a user