sasukeUchiha123 commited on
Commit
bea8b2d
·
verified ·
1 Parent(s): 5b450c1

Upload agent/backends/base.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. 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
+ """