Upload agent/backends/base.py with huggingface_hub
Browse files- agent/backends/base.py +95 -0
agent/backends/base.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Backend protocol — what the agent loop needs from any LLM provider.
|
| 2 |
+
|
| 3 |
+
The loop is provider-agnostic. Each backend owns its own conversation state
|
| 4 |
+
and per-API translation; the loop only sees the unified `AgentTurn` shape.
|
| 5 |
+
|
| 6 |
+
Two backends ship:
|
| 7 |
+
* `ClaudeBackend` — anthropic.AsyncAnthropic, system as a top-level field,
|
| 8 |
+
tool_result blocks batched in one user message.
|
| 9 |
+
* `QwenHFBackend` — huggingface_hub.AsyncInferenceClient.chat_completion,
|
| 10 |
+
system as the first message, tool_result as one role="tool" message per
|
| 11 |
+
call. Routes to whichever provider serves the chosen Qwen model.
|
| 12 |
+
|
| 13 |
+
A future LiveQwenBackend talking to a self-hosted vLLM-on-MI300X endpoint
|
| 14 |
+
slots in identically — it just speaks the OpenAI-compatible shape.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
from __future__ import annotations
|
| 18 |
+
|
| 19 |
+
from abc import ABC, abstractmethod
|
| 20 |
+
from dataclasses import dataclass, field
|
| 21 |
+
from typing import Any
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
@dataclass
|
| 25 |
+
class ToolCall:
|
| 26 |
+
"""One tool call requested by the model in a turn."""
|
| 27 |
+
|
| 28 |
+
id: str
|
| 29 |
+
"""Provider-assigned identifier. Used to correlate the eventual tool_result."""
|
| 30 |
+
name: str
|
| 31 |
+
input: dict[str, Any] = field(default_factory=dict)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@dataclass
|
| 35 |
+
class AgentTurn:
|
| 36 |
+
"""One turn's response, normalized across providers."""
|
| 37 |
+
|
| 38 |
+
text_blocks: list[str] = field(default_factory=list)
|
| 39 |
+
"""Free-text the model produced this turn (rendered as `thought` SSE events)."""
|
| 40 |
+
|
| 41 |
+
tool_calls: list[ToolCall] = field(default_factory=list)
|
| 42 |
+
|
| 43 |
+
stop_reason: str = "end_turn"
|
| 44 |
+
"""One of: 'end_turn', 'tool_use', 'max_tokens', 'other'.
|
| 45 |
+
|
| 46 |
+
The loop breaks on 'end_turn'. Other values keep iterating up to MAX_STEPS.
|
| 47 |
+
"""
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class Backend(ABC):
|
| 51 |
+
"""Pluggable LLM driver for the agent loop.
|
| 52 |
+
|
| 53 |
+
Lifecycle:
|
| 54 |
+
backend = SomeBackend(system_prompt=...)
|
| 55 |
+
backend.add_user_message("Audit this workload: ...")
|
| 56 |
+
for step in range(MAX_STEPS):
|
| 57 |
+
turn = await backend.next_turn(tool_schemas)
|
| 58 |
+
... yield events ...
|
| 59 |
+
for tc in turn.tool_calls:
|
| 60 |
+
result = call_tool(tc)
|
| 61 |
+
backend.add_tool_result(tc.id, tc.name, result.content, is_error=...)
|
| 62 |
+
if turn.stop_reason == "end_turn":
|
| 63 |
+
break
|
| 64 |
+
"""
|
| 65 |
+
|
| 66 |
+
name: str = "base"
|
| 67 |
+
"""Short label used in /healthz and logs (e.g. 'claude', 'qwen-hf')."""
|
| 68 |
+
|
| 69 |
+
@abstractmethod
|
| 70 |
+
def add_user_message(self, content: str) -> None:
|
| 71 |
+
"""Append a user message to the internal conversation."""
|
| 72 |
+
|
| 73 |
+
@abstractmethod
|
| 74 |
+
async def next_turn(self, tool_schemas: list[dict[str, Any]]) -> AgentTurn:
|
| 75 |
+
"""Run one turn against the provider. Updates internal state so the
|
| 76 |
+
next call to `next_turn` already has the assistant's last response in
|
| 77 |
+
context. Tool schemas are in the loop's neutral shape:
|
| 78 |
+
{"name": str, "description": str, "input_schema": json-schema dict}
|
| 79 |
+
Each backend translates to the provider's own tool format.
|
| 80 |
+
"""
|
| 81 |
+
|
| 82 |
+
@abstractmethod
|
| 83 |
+
def add_tool_result(
|
| 84 |
+
self,
|
| 85 |
+
tool_call_id: str,
|
| 86 |
+
name: str,
|
| 87 |
+
content: str,
|
| 88 |
+
is_error: bool,
|
| 89 |
+
) -> None:
|
| 90 |
+
"""Record a tool result for the model to consume on the next turn.
|
| 91 |
+
|
| 92 |
+
Different providers want different shapes (Claude batches into one
|
| 93 |
+
user message; OpenAI/Qwen wants one role='tool' message per call).
|
| 94 |
+
Implementations buffer or append as appropriate.
|
| 95 |
+
"""
|