from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import json
import os
import time
import requests
app = FastAPI(title="Valvomo LLM Proxy")
STATE_FILE = "valvomo_state.json"
# Todellinen LLM-backend (esim. paikallinen Ollama tai OpenAI)
REAL_LLM_URL = "http://localhost:11434/v1/chat/completions"
def load_state():
if not os.path.exists(STATE_FILE):
return {"prompts": {}, "responses": {}}
with open(STATE_FILE, "r", encoding="utf-8") as f:
return json.load(f)
def save_state(state):
with open(STATE_FILE, "w", encoding="utf-8") as f:
json.dump(state, f, indent=4, ensure_ascii=False)
class ChatCompletionRequest(BaseModel):
model: str = "default"
messages: list
temperature: float = 0.7
# ==========================================
# 1. CLIENT-RAJAPINTA (Matkii LLM API:ta)
# ==========================================
def intercept_prompt(req: ChatCompletionRequest):
"""
Sovellukset lC$hettC$vC$t viestinsC$ tC$nne luullen tC$tC$ oikeaksi LLM
API:ksi.
PyyntC6 pysC$ytetC$C$n saapuvien jonoon.
"""
state = load_state()
prompt_id = str(int(time.time() * 1000))
prompt_data = {
"id": prompt_id,
"status": "pending_approval", # pending_approval, approved, rejected,
manual_answered
"request": req.dict(),
"created_at": time.time(),
"manual_response": None
}
state["prompts"][prompt_id] = prompt_data
save_state(state)
# Palautetaan klientille simuloitu "odottaa" -tila tai pidetC$C$n yhteys
auki
# Yksinkertaisuuden vuoksi palautetaan viesti, ettC$ pyyntC6 on jonossa
return {
"id": prompt_id,
"object": "chat.completion",
"choices": [{
"message": {"role": "assistant", "content": "[VALVOMO] Prompti on
siirretty hyvC$ksyntC$jonoon odottamaan tarkastusta."},
"finish_reason": "stop"
}]
}
# ==========================================
# 2. VALVOMON HALLINTARAJAPINTA (Promptit)
# ==========================================
def list_pending_prompts():
"""Listaa odottavat promptit."""
state = load_state()
return {pid: p for pid, p in state["prompts"].items() if p["status"] ==
"pending_approval"}
def edit_pending_prompt(prompt_id: str, new_messages: list):
"""Muokkaa odottavaa promptia ennen sen vapauttamista."""
state = load_state()
if prompt_id not in state["prompts"]:
raise HTTPException(status_code=404, detail="Promptia ei lC6ydy")
state["prompts"][prompt_id]["request"]["messages"] = new_messages
save_state(state)
return {"status": "success", "message": "Promptia muokattu"}
def reject_prompt(prompt_id: str):
"""HylkC$C$ odottava prompti."""
state = load_state()
if prompt_id not in state["prompts"]:
raise HTTPException(status_code=404, detail="Promptia ei lC6ydy")
state["prompts"][prompt_id]["status"] = "rejected"
save_state(state)
return {"status": "success", "message": "Prompti hylC$tty"}
def manual_answer_prompt(prompt_id: str, answer_content: str):
"""Antaa odottavaan promptiin suoraan oman vastauksen ohittaen LLM:n."""
state = load_state()
if prompt_id not in state["prompts"]:
raise HTTPException(status_code=404, detail="Promptia ei lC6ydy")
p = state["prompts"][prompt_id]
p["status"] = "manual_answered"
p["manual_response"] = answer_content
# Luodaan suoraan vastausjonoon valmis vastaus
state["responses"][prompt_id] = {
"id": prompt_id,
"status": "pending_response_approval",
"content": answer_content,
"source": "manual"
}
save_state(state)
return {"status": "success", "message": "Manuaalinen vastaus tallennettu"}
def approve_and_send_to_llm(prompt_id: str):
"""HyvC$ksyy promptin ja lC$hettC$C$ sen eteenpC$in oikealle LLM:lle."""
state = load_state()
if prompt_id not in state["prompts"]:
raise HTTPException(status_code=404, detail="Promptia ei lC6ydy")
p = state["prompts"][prompt_id]
p["status"] = "approved"
# LC$hetetC$C$n pyyntC6 eteenpC$in oikealle LLM:lle (esim. Ollama /
OpenAI)
try:
llm_resp = requests.post(REAL_LLM_URL, json=p["request"], timeout=60)
llm_data = llm_resp.json()
llm_content = llm_data["choices"][0]["message"]["content"]
except Exception as e:
llm_content = f"[VIRHE LLM-KUTSUSSA]: {str(e)}"
# Tallennetaan vastaus UUSIEN VASTAUSTEN JONOON (Outbound Queue)
state["responses"][prompt_id] = {
"id": prompt_id,
"status": "pending_response_approval",
"content": llm_content,
"source": "llm"
}
save_state(state)
return {"status": "success", "message": "Prompti ajettu LLM:ssC$, vastaus
siirretty vastausjonoon tarkastettavaksi."}
# ==========================================
# 3. VALVOMON HALLINTARAJAPINTA (Vastaukset)
# ==========================================
def list_pending_responses():
"""Listaa LLM:n palauttamat vastaukset, jotka odottavat kuratointia/hyvC$ks