spagestic commited on
Commit
de42e4a
·
1 Parent(s): 897a75f

Improve agent orchestration, tool summaries, and trace logging.

Browse files
.env.example CHANGED
@@ -1,2 +1,7 @@
1
  EXA_API_KEY=
2
  FIRECRAWL_API_KEY=
 
 
 
 
 
 
1
  EXA_API_KEY=
2
  FIRECRAWL_API_KEY=
3
+ BORDERLESS_MODEL_ID=Qwen/Qwen3.6-27B
4
+ BORDERLESS_MAX_TOOL_ROUNDS=7
5
+ BORDERLESS_TRACE_DIR=agent_traces
6
+ # Set to 1 if you do not want local JSONL trace logs.
7
+ BORDERLESS_DISABLE_TRACE_LOGS=0
.gitignore CHANGED
@@ -44,6 +44,7 @@ coverage.xml
44
 
45
  # Logs
46
  *.log
 
47
 
48
  # OS
49
  .DS_Store
 
44
 
45
  # Logs
46
  *.log
47
+ agent_traces/
48
 
49
  # OS
50
  .DS_Store
ui/inference/config.py CHANGED
@@ -1,11 +1,13 @@
1
  # ui/inference/config.py
2
  from __future__ import annotations
3
 
 
 
4
  from .tool_schemas import TOOL_SCHEMAS
5
  from .tool_schemas import think as THINK_SCHEMA
6
 
7
- MODEL_ID = "Qwen/Qwen3.6-27B"
8
- MAX_TOOL_ROUNDS = 5
9
 
10
  TOOLS = TOOL_SCHEMAS
11
  THINK_TOOLS = [THINK_SCHEMA]
 
1
  # ui/inference/config.py
2
  from __future__ import annotations
3
 
4
+ import os
5
+
6
  from .tool_schemas import TOOL_SCHEMAS
7
  from .tool_schemas import think as THINK_SCHEMA
8
 
9
+ MODEL_ID = os.environ.get("BORDERLESS_MODEL_ID", "Qwen/Qwen3.6-27B")
10
+ MAX_TOOL_ROUNDS = int(os.environ.get("BORDERLESS_MAX_TOOL_ROUNDS", "7"))
11
 
12
  TOOLS = TOOL_SCHEMAS
13
  THINK_TOOLS = [THINK_SCHEMA]
ui/inference/respond.py CHANGED
@@ -198,8 +198,34 @@ def respond(
198
  yield from yield_response(ui_messages, message, globe_state)
199
  return
200
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  yield from yield_response(
202
  ui_messages,
203
- "I reached the maximum number of tool calls for this request.",
204
  globe_state,
205
  )
 
198
  yield from yield_response(ui_messages, message, globe_state)
199
  return
200
 
201
+ try:
202
+ content, reasoning, _ = complete_turn(
203
+ client,
204
+ api_messages
205
+ + [
206
+ {
207
+ "role": "user",
208
+ "content": (
209
+ "Synthesize the research gathered so far into the best "
210
+ "possible partial answer. Clearly label any items that "
211
+ "still need verification on official sources."
212
+ ),
213
+ }
214
+ ],
215
+ max_tokens=max_tokens,
216
+ temperature=temperature,
217
+ top_p=top_p,
218
+ tools=None,
219
+ )
220
+ answer = content or reasoning
221
+ if answer:
222
+ yield from yield_response(ui_messages, answer, globe_state)
223
+ return
224
+ except Exception:
225
+ pass
226
+
227
  yield from yield_response(
228
  ui_messages,
229
+ "I gathered research but could not finish the full synthesis. Please ask me to summarize the countries already found.",
230
  globe_state,
231
  )
ui/inference/tools.py CHANGED
@@ -7,11 +7,13 @@ from typing import Any
7
 
8
  from gradio import ChatMessage
9
 
 
10
  from apis.exa import search_immigration
11
  from apis.firecrawl import crawl_site, scrape_page
12
  from ui.globe_commands import apply_update_globe
13
 
14
  from .messages import assistant_message_dict
 
15
 
16
 
17
  def truncate(text: str, limit: int = 4000) -> str:
@@ -42,6 +44,8 @@ def _run_tool(
42
  ) -> tuple[Any, dict[str, Any] | None]:
43
  if name == "think":
44
  return {"ok": True}, globe_state
 
 
45
  if name == "search_immigration_info":
46
  return (
47
  search_immigration(
@@ -78,6 +82,84 @@ def _run_tool(
78
  return {"error": f"Unknown tool: {name}"}, globe_state
79
 
80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  def execute_tool_calls(
82
  api_messages: list[dict[str, Any]],
83
  ui_messages: list[ChatMessage],
@@ -90,19 +172,16 @@ def execute_tool_calls(
90
  for tool_call in tool_calls:
91
  tool_name = tool_call["function"]["name"]
92
  tool_args = tool_call["function"]["arguments"]
 
93
  started = time.monotonic()
94
 
95
  if tool_name == "think":
96
- try:
97
- args = json.loads(tool_args or "{}")
98
- except json.JSONDecodeError:
99
- args = {}
100
- thought = str(args.get("thought") or "").strip()
101
 
102
  ui_messages.append(
103
  ChatMessage(
104
  role="assistant",
105
- content="Thinking…",
106
  metadata={
107
  "title": "Thinking",
108
  "status": "pending",
@@ -111,24 +190,18 @@ def execute_tool_calls(
111
  )
112
  yield ui_messages, globe_state
113
 
114
- if thought:
115
- for index in range(len(thought)):
116
- ui_messages[-1] = ChatMessage(
117
- role="assistant",
118
- content=thought[: index + 1],
119
- metadata={
120
- "title": "Thinking",
121
- "status": "pending",
122
- },
123
- )
124
- yield ui_messages, globe_state
125
-
126
  result, globe_state = run_tool(
127
  tool_name,
128
  tool_args,
129
  globe_state=globe_state,
130
  )
131
  duration = time.monotonic() - started
 
 
 
 
 
 
132
  ui_messages[-1] = ChatMessage(
133
  role="assistant",
134
  content=thought if thought else "Thinking complete.",
@@ -150,15 +223,13 @@ def execute_tool_calls(
150
  )
151
  continue
152
 
 
153
  ui_messages.append(
154
  ChatMessage(
155
  role="assistant",
156
- content=(
157
- f"Calling `{tool_name}` with arguments:\n"
158
- f"```json\n{tool_args}\n```"
159
- ),
160
  metadata={
161
- "title": f"🛠️ Used tool {tool_name}",
162
  "status": "pending",
163
  },
164
  )
@@ -171,16 +242,18 @@ def execute_tool_calls(
171
  globe_state=globe_state,
172
  )
173
  duration = time.monotonic() - started
 
 
 
 
 
 
174
 
175
  ui_messages[-1] = ChatMessage(
176
  role="assistant",
177
- content=(
178
- f"Calling `{tool_name}` with arguments:\n"
179
- f"```json\n{tool_args}\n```\n\n"
180
- f"Result:\n```json\n{truncate(result)}\n```"
181
- ),
182
  metadata={
183
- "title": f"🛠️ Used tool {tool_name}",
184
  "status": "done",
185
  "duration": duration,
186
  },
 
7
 
8
  from gradio import ChatMessage
9
 
10
+ from apis.country_profile import get_country_profiles
11
  from apis.exa import search_immigration
12
  from apis.firecrawl import crawl_site, scrape_page
13
  from ui.globe_commands import apply_update_globe
14
 
15
  from .messages import assistant_message_dict
16
+ from .traces import record_tool_trace
17
 
18
 
19
  def truncate(text: str, limit: int = 4000) -> str:
 
44
  ) -> tuple[Any, dict[str, Any] | None]:
45
  if name == "think":
46
  return {"ok": True}, globe_state
47
+ if name == "get_country_profile":
48
+ return get_country_profiles(args["countries"]), globe_state
49
  if name == "search_immigration_info":
50
  return (
51
  search_immigration(
 
82
  return {"error": f"Unknown tool: {name}"}, globe_state
83
 
84
 
85
+ def _parse_arguments(arguments: str) -> dict[str, Any]:
86
+ try:
87
+ parsed = json.loads(arguments or "{}")
88
+ return parsed if isinstance(parsed, dict) else {}
89
+ except json.JSONDecodeError:
90
+ return {}
91
+
92
+
93
+ def _load_result(result: str) -> dict[str, Any]:
94
+ try:
95
+ parsed = json.loads(result)
96
+ return parsed if isinstance(parsed, dict) else {}
97
+ except json.JSONDecodeError:
98
+ return {}
99
+
100
+
101
+ def _join(items: list[str], fallback: str) -> str:
102
+ clean = [str(item) for item in items if item]
103
+ return ", ".join(clean) if clean else fallback
104
+
105
+
106
+ def _pending_tool_message(tool_name: str, args: dict[str, Any]) -> tuple[str, str]:
107
+ if tool_name == "get_country_profile":
108
+ countries = _join(args.get("countries") or [], "selected countries")
109
+ return "Country profiles", f"Looking up country metadata for {countries}."
110
+ if tool_name == "search_immigration_info":
111
+ query = truncate(str(args.get("query") or "immigration sources"), 180)
112
+ return "Searching sources", f"Searching for official immigration information: {query}"
113
+ if tool_name == "scrape_web_page":
114
+ return "Reading official page", f"Reading {args.get('url', 'the selected page')}."
115
+ if tool_name == "crawl_web_site":
116
+ return "Crawling source site", f"Crawling related pages from {args.get('url', 'the selected site')}."
117
+ if tool_name == "update_globe":
118
+ countries = _join(args.get("countries") or [], "the selected countries")
119
+ return "Updating globe", f"Showing {countries} on the globe."
120
+ return f"Using {tool_name}", f"Running `{tool_name}`."
121
+
122
+
123
+ def _done_tool_message(tool_name: str, args: dict[str, Any], result: str) -> str:
124
+ parsed = _load_result(result)
125
+ if parsed.get("error"):
126
+ return f"Tool returned an issue: {parsed['error']}"
127
+
128
+ if tool_name == "get_country_profile":
129
+ countries = [
130
+ country.get("name", "")
131
+ for country in parsed.get("countries", [])
132
+ if isinstance(country, dict)
133
+ ]
134
+ return f"Found country metadata for {_join(countries, 'the selected countries')}."
135
+
136
+ if tool_name == "search_immigration_info":
137
+ count = parsed.get("num_results", 0)
138
+ official = parsed.get("official_results", 0)
139
+ hints = parsed.get("official_domain_hints") or []
140
+ hint_text = f" Suggested official domains: {_join(hints, 'none')}." if hints else ""
141
+ return f"Found {count} search results, including {official} likely official source(s).{hint_text}"
142
+
143
+ if tool_name == "scrape_web_page":
144
+ title = parsed.get("title") or args.get("url") or "the page"
145
+ source = parsed.get("source_url") or parsed.get("url")
146
+ return f"Extracted official page content from {title}. Source: {source}"
147
+
148
+ if tool_name == "crawl_web_site":
149
+ pages = parsed.get("pages_found", 0)
150
+ return f"Collected {pages} related page(s) from the official site."
151
+
152
+ if tool_name == "update_globe":
153
+ countries = [
154
+ country.get("name", country.get("iso2", ""))
155
+ for country in parsed.get("countries", [])
156
+ if isinstance(country, dict)
157
+ ]
158
+ return f"Updated the globe with {_join(countries, 'the selected countries')}."
159
+
160
+ return f"`{tool_name}` completed."
161
+
162
+
163
  def execute_tool_calls(
164
  api_messages: list[dict[str, Any]],
165
  ui_messages: list[ChatMessage],
 
172
  for tool_call in tool_calls:
173
  tool_name = tool_call["function"]["name"]
174
  tool_args = tool_call["function"]["arguments"]
175
+ parsed_args = _parse_arguments(tool_args)
176
  started = time.monotonic()
177
 
178
  if tool_name == "think":
179
+ thought = str(parsed_args.get("thought") or "").strip()
 
 
 
 
180
 
181
  ui_messages.append(
182
  ChatMessage(
183
  role="assistant",
184
+ content="Planning the research approach...",
185
  metadata={
186
  "title": "Thinking",
187
  "status": "pending",
 
190
  )
191
  yield ui_messages, globe_state
192
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  result, globe_state = run_tool(
194
  tool_name,
195
  tool_args,
196
  globe_state=globe_state,
197
  )
198
  duration = time.monotonic() - started
199
+ record_tool_trace(
200
+ tool_name=tool_name,
201
+ arguments=tool_args,
202
+ result=result,
203
+ duration=duration,
204
+ )
205
  ui_messages[-1] = ChatMessage(
206
  role="assistant",
207
  content=thought if thought else "Thinking complete.",
 
223
  )
224
  continue
225
 
226
+ title, pending_message = _pending_tool_message(tool_name, parsed_args)
227
  ui_messages.append(
228
  ChatMessage(
229
  role="assistant",
230
+ content=pending_message,
 
 
 
231
  metadata={
232
+ "title": title,
233
  "status": "pending",
234
  },
235
  )
 
242
  globe_state=globe_state,
243
  )
244
  duration = time.monotonic() - started
245
+ record_tool_trace(
246
+ tool_name=tool_name,
247
+ arguments=tool_args,
248
+ result=result,
249
+ duration=duration,
250
+ )
251
 
252
  ui_messages[-1] = ChatMessage(
253
  role="assistant",
254
+ content=_done_tool_message(tool_name, parsed_args, result),
 
 
 
 
255
  metadata={
256
+ "title": title,
257
  "status": "done",
258
  "duration": duration,
259
  },
ui/inference/traces.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Optional JSONL trace logging for sharing Borderless agent sessions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import time
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ TRACE_DIR = Path(os.environ.get("BORDERLESS_TRACE_DIR", "agent_traces"))
12
+ TRACE_RESULT_LIMIT = 6000
13
+
14
+
15
+ def _compact_result(result: str) -> str:
16
+ if len(result) <= TRACE_RESULT_LIMIT:
17
+ return result
18
+ return result[:TRACE_RESULT_LIMIT] + "\n... (truncated)"
19
+
20
+
21
+ def record_tool_trace(
22
+ *,
23
+ tool_name: str,
24
+ arguments: str,
25
+ result: str,
26
+ duration: float,
27
+ ) -> None:
28
+ """Write one tool call to JSONL so sessions can be shared after a demo."""
29
+ if os.environ.get("BORDERLESS_DISABLE_TRACE_LOGS") == "1":
30
+ return
31
+
32
+ try:
33
+ TRACE_DIR.mkdir(parents=True, exist_ok=True)
34
+ path = TRACE_DIR / time.strftime("borderless-%Y%m%d.jsonl")
35
+ payload: dict[str, Any] = {
36
+ "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
37
+ "tool_name": tool_name,
38
+ "arguments": arguments,
39
+ "duration_seconds": round(duration, 3),
40
+ "result": _compact_result(result),
41
+ }
42
+ with path.open("a", encoding="utf-8") as handle:
43
+ handle.write(json.dumps(payload, ensure_ascii=False) + "\n")
44
+ except OSError:
45
+ # Trace export should never break the live research experience.
46
+ return