Nimbus assistant: chat endpoint with weather and calendar tools

Python · LLM apps · advanced · greenfield

Adds the Nimbus personal-assistant chat endpoint with two internal tools. ConversationHistory (app/history.py) owns the transcript and documents the chat-completions message-order contract; the endpoint (app/agent.py) runs a bounded tool loop: model-produced arguments are validated before a tool runs, a lone tool call is executed inline, and a batch of tool calls in one turn runs concurrently with asyncio.gather. Invalid arguments, unknown tools and service outages come back to the model as tool errors; provider outages surface as a clean 502. Exercised single-lookup questions ("weather in Berlin?", "what's on my calendar tomorrow?") against the staging services and the assistant composed the answers from the returned data.

Nimbus is an internal personal assistant for employees. `weather-svc` and `calendar-svc` are trusted internal HTTP microservices; the model's tool arguments are NOT trusted and are validated before a tool runs. The model routinely batches independent lookups into one turn (parallel function calling) — e.g. comparing the weather in two cities arrives as two `get_weather` calls in a single assistant message — and the system prompt explicitly encourages that.

Requirements

Files touched

--- app/history.py
+"""Conversation history for the assistant agent.
+
+Ordering contract (chat-completions API): within the transcript, every
+``role: "tool"`` message must be preceded by the assistant message that
+carries the matching ``tool_call_id`` in its ``tool_calls``. An assistant
+turn that requests tools is therefore recorded FIRST — via
+:meth:`ConversationHistory.add_assistant` — and only then its results, one
+``add_tool_result`` call per tool call. A transcript where a tool message
+comes before (or without) its assistant ``tool_calls`` message is rejected
+by the API with HTTP 400 on the next request.
+
+This class records messages in the caller's append order; it deliberately
+does not reorder or validate them, so the contract above is the caller's
+responsibility.
+"""
+
+from __future__ import annotations
+
+from openai.types.chat import ChatCompletionMessage
+
+
+class ConversationHistory:
+    """The message list sent with every model call, in append order."""
+
+    def __init__(self, system_prompt: str) -> None:
+        self._messages: list[dict] = [
+            {"role": "system", "content": system_prompt}
+        ]
+

Review this PR

Python practice