first commit
This commit is contained in:
13
relay/Dockerfile
Normal file
13
relay/Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
||||
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY relay.py ./
|
||||
ENV PORT=8090
|
||||
# Reads API key from OPENAI_API_KEY or OPENAI_API_KEY_FILE (/run/secrets/openai_api_key)
|
||||
# AGENT_URL defaults to http://ai-agent:8080
|
||||
# OPENAI_MODEL defaults to gpt-4o-mini
|
||||
CMD ["uvicorn", "relay:app", "--host", "0.0.0.0", "--port", "8090"]
|
||||
11
relay/client.sh
Normal file
11
relay/client.sh
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
#!/usr/bin/env bash
|
||||
# Simple CLI to talk to the relay
|
||||
# Usage: ./client.sh "scale weblabs_php to 3"
|
||||
set -euo pipefail
|
||||
PROMPT="${1:-}"
|
||||
if [ -z "$PROMPT" ]; then
|
||||
echo "Usage: $0 "your request"" >&2
|
||||
exit 1
|
||||
fi
|
||||
curl -s -X POST http://localhost:8090/chat -H 'Content-Type: application/json' -d "$(jq -n --arg p "$PROMPT" '{prompt:$p}')" | jq .
|
||||
76
relay/relay.py
Normal file
76
relay/relay.py
Normal file
@@ -0,0 +1,76 @@
|
||||
|
||||
import os, json, httpx
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
AGENT_URL = os.getenv("AGENT_URL", "http://ai-agent:8080")
|
||||
OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-4o-mini")
|
||||
|
||||
def _read_api_key():
|
||||
# Prefer file from Docker secret if present
|
||||
path = os.getenv("OPENAI_API_KEY_FILE", "/run/secrets/openai_api_key")
|
||||
if os.path.exists(path):
|
||||
return open(path, "r").read().strip()
|
||||
return os.getenv("OPENAI_API_KEY", "")
|
||||
|
||||
SYSTEM_PROMPT = (
|
||||
"You are an ops command planner. Convert the user's intent into a STRICT JSON object "
|
||||
"with fields: action (scale|restart_service), params (dict). No prose. Examples: "
|
||||
'{"action":"scale","params":{"service":"weblabs_php","replicas":3}} '
|
||||
'or {"action":"restart_service","params":{"service":"weblabs_php"}}. '
|
||||
"Only produce valid JSON. If unclear, choose the safest no-op."
|
||||
)
|
||||
|
||||
class ChatIn(BaseModel):
|
||||
prompt: str
|
||||
|
||||
app = FastAPI(title="AI Relay (LLM -> Agent)")
|
||||
|
||||
@app.get("/health")
|
||||
def health():
|
||||
return {"ok": True}
|
||||
|
||||
@app.post("/chat")
|
||||
async def chat(inp: ChatIn):
|
||||
api_key = _read_api_key()
|
||||
if not api_key:
|
||||
raise HTTPException(500, "Missing OPENAI_API_KEY (env or secret).")
|
||||
|
||||
# Call OpenAI Responses API
|
||||
url = "https://api.openai.com/v1/responses"
|
||||
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
||||
body = {
|
||||
"model": OPENAI_MODEL,
|
||||
"input": f"{SYSTEM_PROMPT}\nUSER: {inp.prompt}",
|
||||
"max_output_tokens": 300,
|
||||
"temperature": 0.1
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient(timeout=30) as client:
|
||||
r = await client.post(url, headers=headers, json=body)
|
||||
if r.status_code >= 400:
|
||||
raise HTTPException(502, f"OpenAI error: {r.text}")
|
||||
data = r.json()
|
||||
# Responses API returns output in 'output_text' (or tool messages). Try common fields.
|
||||
content = data.get("output_text") or data.get("content") or ""
|
||||
if isinstance(content, list):
|
||||
# Some responses return a list of content parts; take text from first text part
|
||||
for part in content:
|
||||
if part.get("type") in ("output_text", "text") and part.get("text"):
|
||||
content = part["text"]
|
||||
break
|
||||
if not isinstance(content, str):
|
||||
content = str(content)
|
||||
|
||||
# Parse JSON from the model output
|
||||
try:
|
||||
cmd = json.loads(content)
|
||||
except Exception as e:
|
||||
raise HTTPException(500, f"Failed to parse model JSON: {e}; content={content[:200]}")
|
||||
|
||||
# Forward to the agent
|
||||
async with httpx.AsyncClient(timeout=15) as client:
|
||||
r = await client.post(f"{AGENT_URL}/command", json=cmd)
|
||||
if r.status_code >= 400:
|
||||
raise HTTPException(r.status_code, f"Agent error: {r.text}")
|
||||
return r.json()
|
||||
6
relay/requirements.txt
Normal file
6
relay/requirements.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
|
||||
fastapi==0.115.0
|
||||
uvicorn[standard]==0.30.6
|
||||
httpx==0.27.2
|
||||
pydantic==2.9.2
|
||||
python-dotenv==1.0.1
|
||||
Reference in New Issue
Block a user