spagestic commited on
Commit
4f09ce7
·
1 Parent(s): 968092f

modularized interface and added tools

Browse files
ui/inference.py DELETED
@@ -1,42 +0,0 @@
1
- # ui/inference.py
2
- from huggingface_hub import InferenceClient
3
-
4
- import gradio as gr
5
-
6
- MODEL_ID = "Qwen/Qwen3.6-27B"
7
-
8
-
9
- def respond(
10
- message,
11
- history: list[dict[str, str]],
12
- system_message,
13
- max_tokens,
14
- temperature,
15
- top_p,
16
- hf_token: gr.OAuthToken,
17
- ):
18
- """
19
- For more information on `huggingface_hub` Inference API support, please check the docs: https://huggingface.co/docs/huggingface_hub/v0.22.2/en/guides/inference
20
- """
21
- client = InferenceClient(token=hf_token.token, model=MODEL_ID)
22
-
23
- messages = [{"role": "system", "content": system_message}]
24
- messages.extend(history)
25
- messages.append({"role": "user", "content": message})
26
-
27
- response = ""
28
-
29
- for message in client.chat_completion(
30
- messages,
31
- max_tokens=max_tokens,
32
- stream=True,
33
- temperature=temperature,
34
- top_p=top_p,
35
- ):
36
- choices = message.choices
37
- token = ""
38
- if len(choices) and choices[0].delta.content:
39
- token = choices[0].delta.content
40
-
41
- response += token
42
- yield response
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ui/inference/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ # ui/inference/__init__.py
2
+ from .respond import respond
3
+
4
+ __all__ = ["respond"]
ui/inference/completion.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ui/inference/completion.py
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+ from huggingface_hub import InferenceClient
7
+
8
+ from .messages import parse_text_tool_calls, parse_tool_calls
9
+
10
+
11
+ def complete_turn(
12
+ client: InferenceClient,
13
+ api_messages: list[dict[str, Any]],
14
+ *,
15
+ max_tokens: int,
16
+ temperature: float,
17
+ top_p: float,
18
+ tools: list[dict[str, Any]] | None,
19
+ tool_choice: str = "auto",
20
+ ) -> tuple[str, str, list[dict[str, Any]] | None]:
21
+ """Run one model turn, preferring streaming and falling back to a single request."""
22
+ content = ""
23
+ reasoning = ""
24
+ tool_calls_map: dict[int, dict[str, Any]] = {}
25
+
26
+ stream = client.chat_completion(
27
+ api_messages,
28
+ max_tokens=max_tokens,
29
+ temperature=temperature,
30
+ top_p=top_p,
31
+ tools=tools,
32
+ tool_choice=tool_choice if tools else None,
33
+ stream=True,
34
+ )
35
+ for chunk in stream:
36
+ if not chunk.choices:
37
+ continue
38
+ delta = chunk.choices[0].delta
39
+ if delta.content:
40
+ content += delta.content
41
+ if delta.reasoning:
42
+ reasoning += delta.reasoning
43
+ if delta.tool_calls:
44
+ for tool_call in delta.tool_calls:
45
+ idx = tool_call.index
46
+ if idx not in tool_calls_map:
47
+ tool_calls_map[idx] = {
48
+ "id": tool_call.id,
49
+ "type": tool_call.type,
50
+ "function": {
51
+ "name": tool_call.function.name or "",
52
+ "arguments": tool_call.function.arguments or "",
53
+ },
54
+ }
55
+ continue
56
+ if tool_call.function.name:
57
+ tool_calls_map[idx]["function"]["name"] = tool_call.function.name
58
+ if tool_call.function.arguments:
59
+ tool_calls_map[idx]["function"]["arguments"] += (
60
+ tool_call.function.arguments
61
+ )
62
+
63
+ text_tool_calls = parse_text_tool_calls(content) or parse_text_tool_calls(reasoning)
64
+ tool_calls = list(tool_calls_map.values()) or text_tool_calls
65
+ if content or reasoning or tool_calls:
66
+ if text_tool_calls:
67
+ content = ""
68
+ reasoning = ""
69
+ return content, reasoning, tool_calls
70
+
71
+ response = client.chat_completion(
72
+ api_messages,
73
+ max_tokens=max_tokens,
74
+ temperature=temperature,
75
+ top_p=top_p,
76
+ tools=tools,
77
+ tool_choice=tool_choice if tools else None,
78
+ )
79
+ assistant_msg = response.choices[0].message
80
+ content = assistant_msg.content or ""
81
+ reasoning = assistant_msg.reasoning or ""
82
+ text_tool_calls = parse_text_tool_calls(content) or parse_text_tool_calls(reasoning)
83
+ tool_calls = parse_tool_calls(assistant_msg) or text_tool_calls
84
+ return (
85
+ "" if text_tool_calls else content,
86
+ "" if text_tool_calls else reasoning,
87
+ tool_calls,
88
+ )
ui/inference/config.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ui/inference/config.py
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+ MODEL_ID = "Qwen/Qwen3.6-27B"
7
+ MAX_TOOL_ROUNDS = 5
8
+
9
+ TOOLS: list[dict[str, Any]] = [
10
+ {
11
+ "type": "function",
12
+ "function": {
13
+ "name": "get_economic_indicator",
14
+ "description": (
15
+ "Fetch World Bank economic indicator time series for a country. "
16
+ "Use ISO-3 country codes (e.g. IND for India, HKG, USA, CHN) and "
17
+ "World Bank indicator codes (e.g. NY.GDP.MKTP.CD for GDP in "
18
+ "current US dollars)."
19
+ ),
20
+ "parameters": {
21
+ "type": "object",
22
+ "properties": {
23
+ "indicator": {
24
+ "type": "string",
25
+ "description": "World Bank indicator code, e.g. NY.GDP.MKTP.CD",
26
+ },
27
+ "country": {
28
+ "type": "string",
29
+ "description": "ISO-3 country code, e.g. IND, HKG, USA",
30
+ },
31
+ },
32
+ "required": ["indicator", "country"],
33
+ },
34
+ },
35
+ },
36
+ ]
ui/inference/messages.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ui/inference/messages.py
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import re
6
+ import uuid
7
+ from typing import Any
8
+
9
+ from .config import TOOLS
10
+
11
+
12
+ TOOL_NAMES = {tool["function"]["name"] for tool in TOOLS}
13
+
14
+
15
+ def history_to_api_messages(history: list[dict[str, Any]]) -> list[dict[str, str]]:
16
+ """Convert Gradio chat history to API messages, skipping UI-only tool steps."""
17
+ messages: list[dict[str, str]] = []
18
+ for msg in history:
19
+ if msg.get("metadata"):
20
+ continue
21
+ content = msg.get("content")
22
+ role = msg.get("role")
23
+ if role in ("user", "assistant") and isinstance(content, str) and content:
24
+ messages.append({"role": role, "content": content})
25
+ return messages
26
+
27
+
28
+ def assistant_message_dict(
29
+ content: str,
30
+ tool_calls: list[dict[str, Any]] | None,
31
+ ) -> dict[str, Any]:
32
+ message: dict[str, Any] = {"role": "assistant", "content": content}
33
+ if tool_calls:
34
+ message["tool_calls"] = tool_calls
35
+ return message
36
+
37
+
38
+ def parse_tool_calls(message: Any) -> list[dict[str, Any]] | None:
39
+ if not message.tool_calls:
40
+ return None
41
+ return [
42
+ {
43
+ "id": tool_call.id,
44
+ "type": tool_call.type,
45
+ "function": {
46
+ "name": tool_call.function.name,
47
+ "arguments": tool_call.function.arguments,
48
+ },
49
+ }
50
+ for tool_call in message.tool_calls
51
+ ]
52
+
53
+
54
+ def parse_text_tool_calls(text: str) -> list[dict[str, Any]] | None:
55
+ """Recover tool calls when a model emits them as text instead of tool_calls."""
56
+ if not text:
57
+ return None
58
+
59
+ for tool_name in TOOL_NAMES:
60
+ arguments = _parse_json_call(text, tool_name) or _parse_python_call(
61
+ text, tool_name
62
+ )
63
+ if arguments is None:
64
+ continue
65
+ return [
66
+ {
67
+ "id": f"call_{uuid.uuid4().hex}",
68
+ "type": "function",
69
+ "function": {
70
+ "name": tool_name,
71
+ "arguments": json.dumps(arguments),
72
+ },
73
+ }
74
+ ]
75
+
76
+ return None
77
+
78
+
79
+ def _parse_json_call(text: str, tool_name: str) -> dict[str, Any] | None:
80
+ match = re.search(rf"(?:default_api:)?{re.escape(tool_name)}\s*\{{", text)
81
+ if not match:
82
+ return None
83
+
84
+ start = text.find("{", match.start())
85
+ if start == -1:
86
+ return None
87
+
88
+ decoder = json.JSONDecoder()
89
+ try:
90
+ arguments, _ = decoder.raw_decode(text[start:])
91
+ except json.JSONDecodeError:
92
+ return _parse_key_values(text[start:])
93
+
94
+ return arguments if isinstance(arguments, dict) else None
95
+
96
+
97
+ def _parse_python_call(text: str, tool_name: str) -> dict[str, Any] | None:
98
+ match = re.search(rf"{re.escape(tool_name)}\s*\((?P<args>[^)]*)\)", text)
99
+ if not match:
100
+ return None
101
+ return _parse_key_values(match.group("args"))
102
+
103
+
104
+ def _parse_key_values(text: str) -> dict[str, str] | None:
105
+ pairs = re.findall(r'(\w+)\s*=\s*["\']([^"\']+)["\']', text)
106
+ if not pairs:
107
+ pairs = re.findall(r'["\'](\w+)["\']\s*:\s*["\']([^"\']+)["\']', text)
108
+ if not pairs:
109
+ return None
110
+ return {key: value for key, value in pairs}
ui/inference/respond.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ui/inference/respond.py
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+ import gradio as gr
7
+ from gradio import ChatMessage
8
+ from huggingface_hub import InferenceClient
9
+
10
+ from .completion import complete_turn
11
+ from .config import MAX_TOOL_ROUNDS, MODEL_ID, TOOLS
12
+ from .messages import history_to_api_messages
13
+ from .streaming import yield_response
14
+ from .tools import execute_tool_calls
15
+
16
+
17
+ def respond(
18
+ message,
19
+ history: list[dict[str, str]],
20
+ system_message,
21
+ max_tokens,
22
+ temperature,
23
+ top_p,
24
+ hf_token: gr.OAuthToken,
25
+ ):
26
+ """
27
+ For more information on `huggingface_hub` Inference API support, please check the docs: https://huggingface.co/docs/huggingface_hub/v0.22.2/en/guides/inference
28
+ """
29
+ client = InferenceClient(token=hf_token.token, model=MODEL_ID)
30
+
31
+ api_messages: list[dict[str, Any]] = [
32
+ {"role": "system", "content": system_message},
33
+ *history_to_api_messages(history),
34
+ {"role": "user", "content": message},
35
+ ]
36
+ ui_messages: list[ChatMessage] = []
37
+
38
+ try:
39
+ for _ in range(MAX_TOOL_ROUNDS):
40
+ content, reasoning, tool_calls = complete_turn(
41
+ client,
42
+ api_messages,
43
+ max_tokens=max_tokens,
44
+ temperature=temperature,
45
+ top_p=top_p,
46
+ tools=TOOLS,
47
+ tool_choice="auto",
48
+ )
49
+
50
+ if tool_calls:
51
+ yield from execute_tool_calls(
52
+ api_messages, ui_messages, tool_calls, content
53
+ )
54
+ continue
55
+
56
+ answer = content or reasoning
57
+ if answer:
58
+ yield from yield_response(ui_messages, answer)
59
+ return
60
+
61
+ # Retry once with a required tool call when the model returned nothing.
62
+ content, reasoning, tool_calls = complete_turn(
63
+ client,
64
+ api_messages,
65
+ max_tokens=max_tokens,
66
+ temperature=temperature,
67
+ top_p=top_p,
68
+ tools=TOOLS,
69
+ tool_choice="required",
70
+ )
71
+ if tool_calls:
72
+ yield from execute_tool_calls(
73
+ api_messages, ui_messages, tool_calls, content
74
+ )
75
+ continue
76
+
77
+ answer = content or reasoning
78
+ if answer:
79
+ yield from yield_response(ui_messages, answer)
80
+ return
81
+
82
+ # Some providers return an empty first turn when tools are enabled.
83
+ content, reasoning, _ = complete_turn(
84
+ client,
85
+ api_messages,
86
+ max_tokens=max_tokens,
87
+ temperature=temperature,
88
+ top_p=top_p,
89
+ tools=None,
90
+ )
91
+ answer = content or reasoning
92
+ if answer:
93
+ yield from yield_response(ui_messages, answer)
94
+ return
95
+
96
+ yield from yield_response(
97
+ ui_messages,
98
+ "I could not generate a response. Please try again.",
99
+ )
100
+ return
101
+
102
+ except Exception as exc:
103
+ yield from yield_response(
104
+ ui_messages,
105
+ f"Sorry, something went wrong while generating a response: {exc}",
106
+ )
107
+ return
108
+
109
+ yield from yield_response(
110
+ ui_messages,
111
+ "I reached the maximum number of tool calls for this request.",
112
+ )
ui/inference/streaming.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ui/inference/streaming.py
2
+ from __future__ import annotations
3
+
4
+ from gradio import ChatMessage
5
+
6
+
7
+ def yield_streaming_string(text: str):
8
+ if not text:
9
+ yield ""
10
+ return
11
+ for index in range(len(text)):
12
+ yield text[: index + 1]
13
+
14
+
15
+ def yield_streaming_messages(ui_messages: list[ChatMessage], text: str):
16
+ messages = list(ui_messages)
17
+ if not text:
18
+ messages.append(ChatMessage(role="assistant", content=""))
19
+ yield messages
20
+ return
21
+
22
+ messages.append(ChatMessage(role="assistant", content=""))
23
+ for index in range(len(text)):
24
+ messages[-1] = ChatMessage(role="assistant", content=text[: index + 1])
25
+ yield messages
26
+
27
+
28
+ def yield_response(ui_messages: list[ChatMessage], text: str):
29
+ if ui_messages:
30
+ yield from yield_streaming_messages(ui_messages, text)
31
+ else:
32
+ yield from yield_streaming_string(text)
ui/inference/tools.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ui/inference/tools.py
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import time
6
+ from typing import Any
7
+
8
+ from gradio import ChatMessage
9
+
10
+ from apis.world_bank import get_indicator_series
11
+
12
+ from .messages import assistant_message_dict
13
+
14
+
15
+ def truncate(text: str, limit: int = 4000) -> str:
16
+ if len(text) <= limit:
17
+ return text
18
+ return text[:limit] + "\n… (truncated)"
19
+
20
+
21
+ def run_tool(name: str, arguments: str) -> str:
22
+ if name != "get_economic_indicator":
23
+ return json.dumps({"error": f"Unknown tool: {name}"})
24
+
25
+ try:
26
+ args = json.loads(arguments or "{}")
27
+ observations = get_indicator_series(
28
+ indicator=args["indicator"],
29
+ country=args["country"],
30
+ )
31
+ return json.dumps(observations, default=str)
32
+ except Exception as exc:
33
+ return json.dumps({"error": str(exc)})
34
+
35
+
36
+ def execute_tool_calls(
37
+ api_messages: list[dict[str, Any]],
38
+ ui_messages: list[ChatMessage],
39
+ tool_calls: list[dict[str, Any]],
40
+ content: str,
41
+ ):
42
+ api_messages.append(assistant_message_dict(content, tool_calls))
43
+
44
+ for tool_call in tool_calls:
45
+ tool_name = tool_call["function"]["name"]
46
+ tool_args = tool_call["function"]["arguments"]
47
+ started = time.monotonic()
48
+
49
+ ui_messages.append(
50
+ ChatMessage(
51
+ role="assistant",
52
+ content=(
53
+ f"Calling `{tool_name}` with arguments:\n"
54
+ f"```json\n{tool_args}\n```"
55
+ ),
56
+ metadata={
57
+ "title": f"🛠️ Used tool {tool_name}",
58
+ "status": "pending",
59
+ },
60
+ )
61
+ )
62
+ yield ui_messages
63
+
64
+ result = run_tool(tool_name, tool_args)
65
+ duration = time.monotonic() - started
66
+
67
+ ui_messages[-1] = ChatMessage(
68
+ role="assistant",
69
+ content=(
70
+ f"Calling `{tool_name}` with arguments:\n"
71
+ f"```json\n{tool_args}\n```\n\n"
72
+ f"Result:\n```json\n{truncate(result)}\n```"
73
+ ),
74
+ metadata={
75
+ "title": f"🛠️ Used tool {tool_name}",
76
+ "status": "done",
77
+ "duration": duration,
78
+ },
79
+ )
80
+ yield ui_messages
81
+
82
+ api_messages.append(
83
+ {
84
+ "role": "tool",
85
+ "tool_call_id": tool_call["id"],
86
+ "name": tool_name,
87
+ "content": result,
88
+ }
89
+ )