Repository navigation
Expand file tree
/
Copy pathagent_as_tools.py
More file actions
417 lines (364 loc) · 14.1 KB
/
Copy pathagent_as_tools.py
File metadata and controls
417 lines (364 loc) · 14.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
import json
from typing import Any, Dict, Optional
from langchain_core.tools import tool
from langchain_core.callbacks import BaseCallbackHandler
from langchain.agents import create_agent
from langgraph.config import get_stream_writer
from backend.shared.logger import get_logger
from backend.core.prompts import (
finance_agent_prompt,
file_agent_prompt,
plotting_prompt,
tcmb_data_agent_prompt,
)
from langchain.agents.middleware import ToolCallLimitMiddleware
from datetime import datetime
from backend.core.runner import extract_query_from_args, extract_text_tokens
from .file_tools import (
read_excel_file,
read_word_document,
read_powerpoint_file,
describe_file_image,
execute_file_code,
list_created_files,
get_available_charts,
)
from .tcmb_data import search_tcmb_series, get_tcmb_data
from .finance import (
get_eod_data,
get_intraday_data,
get_exchanges,
get_exchange_info,
get_currencies,
get_timezones,
get_splits_data,
get_dividends_data,
get_index_list,
get_index_info,
get_tickers_list,
get_ticker_info_detailed,
)
from backend.shared.constants import CURRENT_MODEL
from backend.core.tools.plotting import (
create_custom_chart_from_code,
create_financial_stock_chart,
)
file_tools = [
read_excel_file,
read_word_document,
read_powerpoint_file,
describe_file_image,
execute_file_code,
list_created_files,
get_available_charts,
]
tcmb_tools = [
search_tcmb_series,
get_tcmb_data,
]
finance_tools = [
get_eod_data,
#get_intraday_data,
get_exchanges,
get_exchange_info,
get_currencies,
get_timezones,
get_splits_data,
get_dividends_data,
get_index_list,
get_index_info,
get_tickers_list,
get_ticker_info_detailed,
]
# Create subagents using create_agent
finance_agent = create_agent(
model=CURRENT_MODEL,
tools=finance_tools,
system_prompt=finance_agent_prompt,
)
file_agent = create_agent(
model=CURRENT_MODEL,
tools=file_tools,
system_prompt=file_agent_prompt,
middleware=[
ToolCallLimitMiddleware(
tool_name="execute_file_code",
run_limit=2,
exit_behavior="continue",
),
],
)
plotting_agent = create_agent(
model=CURRENT_MODEL,
tools=[
create_custom_chart_from_code,
create_financial_stock_chart,
],
system_prompt=plotting_prompt,
middleware=[
ToolCallLimitMiddleware(
tool_name="create_custom_chart_from_code",
run_limit=1,
exit_behavior="continue",
),
ToolCallLimitMiddleware(
tool_name="create_financial_stock_chart",
run_limit=1,
exit_behavior="continue",
),
],
)
tcmb_data_agent = create_agent(
model=CURRENT_MODEL,
tools=tcmb_tools,
system_prompt=tcmb_data_agent_prompt.format(
current_datetime=datetime.now().strftime("%Y-%m-%d %H:%M:%S")
),
)
logger = get_logger("SUBAGENT_STREAMING")
class InnerToolCallbackHandler(BaseCallbackHandler):
"""
Callback handler that captures inner tool calls and queues them
for later emission to the parent stream.
"""
def __init__(self, agent_name: str, event_queue: list):
self.agent_name = agent_name
self.event_queue = event_queue
self.current_tool = None
def _parse_input_data(self, input_str: str, kwargs: Dict) -> tuple[Optional[Dict], Optional[str]]:
"""Parse input data from various sources. Returns (input_data, simple_query)."""
inputs = kwargs.get("inputs", {})
if isinstance(inputs, dict) and inputs:
return inputs, None
if isinstance(input_str, dict):
return input_str, None
if isinstance(input_str, str) and input_str:
if input_str.startswith("{"):
try:
return json.loads(input_str), None
except json.JSONDecodeError:
pass
elif not input_str.startswith("<") and len(input_str) < 300:
return None, input_str
return None, None
def _extract_tool_specific_query(self, tool_name: str, input_data: Dict) -> Optional[str]:
"""Extract query with special formatting for specific tools."""
if tool_name == "get_tcmb_data":
serie_codes = input_data.get("serie_codes")
if serie_codes:
codes_str = str(serie_codes) if isinstance(serie_codes, list) else serie_codes
start_date = input_data.get("start_date")
end_date = input_data.get("end_date")
date_range = f", {start_date} to {end_date}" if start_date and end_date else ""
return f"{codes_str}{date_range}"
elif tool_name == "search_tcmb_series":
query = input_data.get("query")
if query:
return f"query: {query}"
elif tool_name == "create_financial_stock_chart":
symbols = input_data.get("symbols")
if symbols:
symbols_str = ", ".join(symbols) if isinstance(symbols, list) else symbols
chart_type = input_data.get("chart_type", "candlestick")
period = input_data.get("period", "daily")
return f"{symbols_str} ({chart_type}, {period})"
elif tool_name in ("get_eod_data", "get_intraday_data", "get_splits_data", "get_dividends_data"):
symbols = input_data.get("symbols")
if symbols:
return f"symbols: {symbols}"
return None
def on_tool_start(
self,
serialized: Dict[str, Any],
input_str: str,
**kwargs: Any,
) -> None:
"""Called when a tool starts."""
tool_name = serialized.get("name", "unknown")
self.current_tool = tool_name
query = None
try:
input_data, simple_query = self._parse_input_data(input_str, kwargs)
if simple_query:
query = simple_query
elif input_data and isinstance(input_data, dict):
# Try tool-specific extraction first
query = self._extract_tool_specific_query(tool_name, input_data)
# Fall back to generic extraction
if not query:
query = extract_query_from_args(input_data)
except Exception:
pass
event_data = {
"event": "inner_tool_start",
"agent": self.agent_name,
"tool_name": tool_name,
}
if query:
event_data["query"] = query[:200] if len(str(query)) > 200 else str(query)
self.event_queue.append(event_data)
def on_tool_end(
self,
output: str,
**kwargs: Any,
) -> None:
"""Called when a tool ends."""
tool_name = self.current_tool or "unknown"
self.event_queue.append({
"event": "inner_tool_end",
"agent": self.agent_name,
"tool_name": tool_name,
})
self.current_tool = None
def on_tool_error(
self,
error: BaseException,
**kwargs: Any,
) -> None:
"""Called when a tool errors - queue inner_tool_end event with error."""
tool_name = self.current_tool or "unknown"
logger.warning(f"[{self.agent_name}] Callback: Inner tool error: {tool_name} - {error}")
self.event_queue.append({
"event": "inner_tool_end",
"agent": self.agent_name,
"tool_name": tool_name,
})
self.current_tool = None
# Helper function to stream sub-agent and emit inner tool events
async def stream_subagent_with_events(agent, agent_name: str, query: str):
"""
Stream a sub-agent's execution and emit custom events for inner tool calls.
Uses callbacks to capture events and emits them via writer during stream iteration.
Returns the final content from the agent.
"""
try:
writer = get_stream_writer()
except Exception as e:
# Not in streaming context - fall back to simple invoke
logger.warning(f"Could not get stream writer for {agent_name}: {e}")
result = await agent.ainvoke(
{"messages": [{"role": "user", "content": query}]}
)
return result["messages"][-1].content
final_content = None
# Shared event queue - callbacks will append events here
event_queue = []
callback_handler = InnerToolCallbackHandler(agent_name, event_queue)
writer({"event": "subagent_start", "agent": agent_name, "query": query[:100]})
# Stream the sub-agent's updates with callback for tool events
async for sub_chunk in agent.astream(
{"messages": [{"role": "user", "content": query}]},
config={"callbacks": [callback_handler]},
stream_mode="updates",
):
# Emit any queued events from callbacks
while event_queue:
event = event_queue.pop(0)
writer(event)
# Process chunks to capture final content
if isinstance(sub_chunk, dict):
for node_name, node_data in sub_chunk.items():
if node_name in ("agent", "model") and isinstance(node_data, dict):
for msg in node_data.get("messages", []):
if hasattr(msg, "content") and msg.content:
# Use shared function for content extraction
for text in extract_text_tokens(msg.content):
final_content = text
# Emit any remaining queued events
while event_queue:
writer(event_queue.pop(0))
writer({"event": "subagent_end", "agent": agent_name})
return final_content
# Wrap subagents as tools for the main agent
@tool(
"finance_agent",
description=(
"Use this tool ONLY when specific financial market data is required. "
"This tool retrieves and analyzes structured financial market data, including "
"end-of-day prices (OHLCV), dividends, splits, indexes, ticker and exchange metadata, "
"currencies, timezones, and historical time-series. "
"It supports multi-symbol and multi-timeframe analysis over this data. "
"Input must be a natural language request describing the desired financial data "
"(e.g., 'get daily OHLCV for AAPL for the last 30 days')."
),
response_format="content_and_artifact",
)
async def call_finance_agent(query: str) -> str:
"""Route a financial data or analysis request to the finance specialist agent.
Args:
query: Natural language request about financial data retrieval or analysis.
"""
content = await stream_subagent_with_events(finance_agent, "finance_agent", query)
artifact = {
"name": "Marketstack API",
"description": f"Financial data: {query[:50]}...",
}
return content, artifact
@tool(
"file_operations",
description=(
"Use this tool for file and document operations: creating, reading, modifying, "
"and analyzing Excel workbooks, Word documents, PowerPoint presentations, and images. "
"It can generate new files, read existing file contents, modify documents, "
"understand images via vision AI, and run Python code in a sandbox for complex file tasks. "
"Input must be a natural language request describing the desired file operation "
"(for example, 'create an Excel file with this table' or 'read the contents of report.docx')."
),
)
async def call_file_agent(query: str) -> str:
"""Route file and document requests to the file operations agent.
Args:
query: Natural language request describing a file operation.
"""
content = await stream_subagent_with_events(file_agent, "file_operations", query)
return content
@tool(
"plotting_agent",
description=(
"Use this tool when a visual chart or plot is explicitly required to answer the user’s request. "
"This tool creates data visualizations, including financial market charts "
"(candlestick, OHLC, line, area with technical indicators, volume) and custom non-financial charts "
"(statistical, scientific, or exploratory) from Python code."
"Input must be a natural language request describing the chart needed "
"and whether the data is financial market data or custom/tabular data."),
)
async def call_plotting_agent(query: str) -> str:
"""Route chart and visualization requests to the plotting agent.
Args:
query: Natural language description of the chart or visualization to create.
"""
content = await stream_subagent_with_events(plotting_agent, "plotting_agent", query)
return content
@tool(
"tcmb_economic_data",
description=(
"Use this tool ONLY to retrieve structured Turkish Central Bank (TCMB) "
"economic data from the EVDS system."
"It provides time-series for economic indicators such as "
"exchange rates, interest rates, inflation, balance of payments, reserves, "
"money and credit statistics, price indices, and survey data. "
"Input must be a natural language query specifying the indicator(s) and "
"time period of interest "
"(e.g., 'monthly CPI inflation and policy rate for the last five years'). "
"Best used for queries requiring official Turkish macroeconomic or monetary data."
),
response_format="content_and_artifact",
)
async def call_tcmb_agent(query: str):
"""Route Turkish economic data requests to the TCMB specialist agent.
Args:
query: Natural language request describing the TCMB/EVDS data to retrieve or analyze.
"""
content = await stream_subagent_with_events(tcmb_data_agent, "tcmb_economic_data", query)
artifact = {
"name": "TCMB EVDS",
"description": f"Turkish economic data: {query[:50]}...",
}
return content, artifact
# List of subagent tools for the main agent
main_agent_subagents = [
call_finance_agent,
call_file_agent,
call_plotting_agent,
call_tcmb_agent,
]