Service — openai_service.py
Every direct OpenAI chat-completion call in the platform funnels through this module's four functions. If you want to change how the platform talks to the LLM, you change it here.
Purpose
backend/app/services/openai_service.py owns the OpenAI client, the model selection, and four prompt-bearing functions. Routers and other services call these; nothing else in the codebase constructs chat completions (the multi-agent service has its own two calls for reply/review — see its page).
Module setup
load_dotenv()
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-4o-mini")Every function begins with the same guard — raising ValueError("OPENAI_API_KEY is missing...") when the key is absent — so failures are explicit rather than cryptic SDK errors. How each caller handles that: Error Handling.
Public API
generate_agent_response(system_prompt, user_message, agent_name, language="English", tone="Professional") -> str
Powers POST /agents/{id}/chat. Composes a runtime system prompt around the agent's stored instructions:
You are {agent_name}.
Agent instructions: {system_prompt}
Language: {language}
Tone: {tone}
Rules: Answer clearly · Be helpful and practical ·
If you do not know the answer, say you do not know · Do not invent facts.Temperature 0.3. Returns the raw completion text. Full prompt text: Prompt Catalog.
analyze_email(email_text) -> dict
Powers email analysis (both single-agent and the workflow's Analysis Agent). A strict-JSON system prompt demands:
{
"summary": "...", "intent": "...",
"priority": "low | medium | high",
"deadline": "... or null",
"action_items": ["..."],
"suggested_reply": "..."
}Temperature 0.2. The response is parsed with json.loads; a parse failure raises ValueError("OpenAI did not return valid JSON: ...") — no retry, by design (Design Decisions).
generate_rag_answer(question, contexts, agent_name, language, tone) -> str
Powers the RAG answer node. Formats each retrieved chunk as a numbered source block (filename, chunk index, content), then instructs the model to answer only from that context, refusing with the exact string "I could not find this information in the uploaded documents." when the context lacks the answer. Temperature 0.2.
The system_prompt/user_prompt assignments are indented inside the for loop that formats sources. The behavior is correct for non-empty contexts (each iteration rebuilds the prompt; the last iteration's version — containing the full accumulated context_text — is used), but with an empty contexts list the variables are never assigned and the function raises NameError. In practice the corrective RAG graph routes empty retrievals to the refusal node so this path is unreachable today — but it is a real landmine for new callers. Tracked in Known Issues.
evaluate_retrieved_context(question, contexts) -> dict
The corrective-RAG evaluator (temperature 0). Asks the model to judge — without answering — whether the retrieved context can answer the question, returning strict JSON that the function normalizes defensively:
return {
"has_answer": bool(result.get("has_answer")),
"confidence": float(result.get("confidence", 0)),
"reason": result.get("reason", ""),
}Used by evaluate_node in the RAG graph to route between answering and refusing (Evaluation).
Inputs / outputs summary
| Function | Input | Output | Temp | JSON-strict |
|---|---|---|---|---|
generate_agent_response | prompt parts + user message | free text | 0.3 | no |
analyze_email | raw email text | analysis dict | 0.2 | yes |
generate_rag_answer | question + context chunks | grounded text | 0.2 | no |
evaluate_retrieved_context | question + context chunks | verdict dict | 0 | yes |
Performance & cost notes
Each call is one synchronous HTTPS round trip (0.5–3 s typical) that blocks the worker thread. Token/cost figures logged alongside these calls come from monitoring_service, not from the API response — they are estimates.
Security notes
The API key never leaves this layer; prompts include user-controlled content (email text, questions, document chunks) — see the prompt-injection discussion in Security.
Future improvements
- Fix the loop-indentation quirk in
generate_rag_answer(and add an explicit empty-context guard). - Use OpenAI structured outputs for the JSON functions; add bounded retries on parse failure.
- Capture actual token usage from
response.usageinstead of estimating.
Related files
- Prompt Catalog — all prompt texts verbatim
- Service: Multi-Agent Email — the other two LLM calls
- AI Overview