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

Upload agent/backends/qwen_hf.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. agent/backends/qwen_hf.py +181 -0
agent/backends/qwen_hf.py ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """QwenHFBackend — drives Qwen via huggingface_hub.AsyncInferenceClient.
2
+
3
+ Routes through Hugging Face Inference Providers (Together / Fireworks /
4
+ Replicate / Nebius / etc., with `provider="auto"` doing the picking).
5
+ The chat_completion API matches OpenAI shape, which is what Qwen tool
6
+ calling speaks natively.
7
+
8
+ Conversation shape (OpenAI-compatible):
9
+ * system as the first message: {"role": "system", "content": "..."}
10
+ * user / assistant alternation
11
+ * tool_calls live on the assistant message: {"role": "assistant",
12
+ "content": "...", "tool_calls": [{"id": ..., "type": "function",
13
+ "function": {"name": ..., "arguments": "<json string>"}}]}
14
+ * tool results: one message each: {"role": "tool", "tool_call_id": ...,
15
+ "content": "..."}
16
+
17
+ Tool schemas (in this codebase's neutral shape) translate to:
18
+ {"type": "function", "function": {"name": ..., "description": ...,
19
+ "parameters": <JSON-schema>}}
20
+
21
+ If a tool result is an error, we prefix the content with `ERROR: ` so the
22
+ model sees that the call failed (OpenAI shape has no `is_error` flag).
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import json
28
+ import os
29
+ from typing import Any
30
+
31
+ from huggingface_hub import AsyncInferenceClient
32
+
33
+ from agent.backends.base import AgentTurn, Backend, ToolCall
34
+
35
+ DEFAULT_MODEL = "Qwen/Qwen2.5-7B-Instruct"
36
+ DEFAULT_MAX_TOKENS = 2048
37
+ DEFAULT_PROVIDER = "auto"
38
+
39
+
40
+ class QwenHFBackend(Backend):
41
+ """Qwen-via-HF-Inference-Providers driver."""
42
+
43
+ name = "qwen-hf"
44
+
45
+ def __init__(
46
+ self,
47
+ system_prompt: str,
48
+ model: str | None = None,
49
+ provider: str | None = None,
50
+ max_tokens: int = DEFAULT_MAX_TOKENS,
51
+ ) -> None:
52
+ self._system = system_prompt
53
+ self._model = model or os.environ.get("GOBLIN_QWEN_MODEL", DEFAULT_MODEL)
54
+ self._provider = provider or os.environ.get(
55
+ "GOBLIN_QWEN_PROVIDER", DEFAULT_PROVIDER
56
+ )
57
+ self._max_tokens = max_tokens
58
+ self._client = self._build_client()
59
+ # System message lives at the head of the conversation for OpenAI-shape
60
+ # APIs. We seed it once at construction.
61
+ self._conversation: list[dict[str, Any]] = [
62
+ {"role": "system", "content": system_prompt}
63
+ ]
64
+
65
+ @staticmethod
66
+ def _build_client() -> AsyncInferenceClient:
67
+ token = os.environ.get("HF_TOKEN") or os.environ.get(
68
+ "HUGGINGFACEHUB_API_TOKEN"
69
+ )
70
+ if not token:
71
+ raise RuntimeError(
72
+ "HF_TOKEN (or HUGGINGFACEHUB_API_TOKEN) is not set; Qwen backend "
73
+ "cannot reach HF Inference Providers. Set the env var, switch to "
74
+ "GOBLIN_AGENT_BACKEND=claude, or use the offline replay UI lane."
75
+ )
76
+ # `provider` is set per-call rather than on the client to keep the
77
+ # constructor stable across huggingface_hub versions.
78
+ return AsyncInferenceClient(token=token)
79
+
80
+ # ------------------------------------------------------------------
81
+ # Backend API
82
+ # ------------------------------------------------------------------
83
+
84
+ def add_user_message(self, content: str) -> None:
85
+ self._conversation.append({"role": "user", "content": content})
86
+
87
+ def add_tool_result(
88
+ self,
89
+ tool_call_id: str,
90
+ name: str, # noqa: ARG002 — OpenAI shape correlates by tool_call_id only
91
+ content: str,
92
+ is_error: bool,
93
+ ) -> None:
94
+ if is_error and not content.startswith("ERROR:"):
95
+ content = f"ERROR: {content}"
96
+ self._conversation.append(
97
+ {
98
+ "role": "tool",
99
+ "tool_call_id": tool_call_id,
100
+ "content": content,
101
+ }
102
+ )
103
+
104
+ async def next_turn(self, tool_schemas: list[dict[str, Any]]) -> AgentTurn:
105
+ oai_tools = _to_openai_tools(tool_schemas)
106
+
107
+ response = await self._client.chat_completion(
108
+ model=self._model,
109
+ messages=self._conversation,
110
+ tools=oai_tools,
111
+ max_tokens=self._max_tokens,
112
+ tool_choice="auto",
113
+ )
114
+
115
+ choice = response.choices[0]
116
+ msg = choice.message
117
+ text = (msg.content or "").strip()
118
+
119
+ # Echo assistant turn so the next request preserves tool_calls.
120
+ echoed: dict[str, Any] = {"role": "assistant", "content": msg.content or ""}
121
+ if msg.tool_calls:
122
+ echoed["tool_calls"] = [
123
+ {
124
+ "id": tc.id,
125
+ "type": "function",
126
+ "function": {
127
+ "name": tc.function.name,
128
+ "arguments": tc.function.arguments or "{}",
129
+ },
130
+ }
131
+ for tc in msg.tool_calls
132
+ ]
133
+ self._conversation.append(echoed)
134
+
135
+ # Translate to neutral AgentTurn.
136
+ text_blocks = [text] if text else []
137
+ tool_calls: list[ToolCall] = []
138
+ for tc in msg.tool_calls or []:
139
+ try:
140
+ args = json.loads(tc.function.arguments) if tc.function.arguments else {}
141
+ except (TypeError, json.JSONDecodeError):
142
+ args = {}
143
+ tool_calls.append(
144
+ ToolCall(id=tc.id, name=tc.function.name, input=args)
145
+ )
146
+
147
+ return AgentTurn(
148
+ text_blocks=text_blocks,
149
+ tool_calls=tool_calls,
150
+ stop_reason=_normalize_finish_reason(choice.finish_reason),
151
+ )
152
+
153
+
154
+ def _to_openai_tools(tool_schemas: list[dict[str, Any]]) -> list[dict[str, Any]]:
155
+ """Translate this codebase's neutral tool schema (the `tool_schemas()`
156
+ shape with `name`/`description`/`input_schema`) into OpenAI's
157
+ `{type: function, function: {...}}` shape that vLLM and HF chat_completion
158
+ consume.
159
+ """
160
+ return [
161
+ {
162
+ "type": "function",
163
+ "function": {
164
+ "name": s["name"],
165
+ "description": s.get("description", ""),
166
+ "parameters": s.get("input_schema") or {"type": "object", "properties": {}},
167
+ },
168
+ }
169
+ for s in tool_schemas
170
+ ]
171
+
172
+
173
+ def _normalize_finish_reason(reason: str | None) -> str:
174
+ """Map OpenAI finish_reason to our neutral set."""
175
+ if reason == "stop":
176
+ return "end_turn"
177
+ if reason == "tool_calls":
178
+ return "tool_use"
179
+ if reason == "length":
180
+ return "max_tokens"
181
+ return reason or "other"