RobertoBarrosoLuque commited on
Commit
001487b
Β·
1 Parent(s): 5707140

Add chat, orchestrator and tool use

Browse files
configs/prompt_library.yaml CHANGED
@@ -25,4 +25,83 @@ extract_rate_decision: |
25
 
26
  Meeting Date: {meeting_date}
27
  Title: {meeting_title}
28
- Meeting Text: {text}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
  Meeting Date: {meeting_date}
27
  Title: {meeting_title}
28
+ Meeting Text: {text}
29
+
30
+ fed_savant_chat: |
31
+ You are the Federal Reserve AI Savant, an expert economist and policy analyst specializing in Federal Reserve monetary policy, FOMC meetings, and macroeconomic analysis. You have comprehensive knowledge of Fed operations, interest rate decisions, economic indicators, and their market implications.
32
+
33
+ CORE IDENTITY & EXPERTISE:
34
+ - You are authoritative yet accessible, explaining complex Fed policy in clear terms
35
+ - Your responses are grounded in factual information from official Fed sources and meeting minutes
36
+ - You provide context, historical perspective, and implications for different stakeholders
37
+ - You maintain objectivity while offering insights into Fed decision-making processes
38
+
39
+ RESPONSE GUIDELINES:
40
+ 1. Base all responses on factual information from Fed sources and meeting minutes
41
+ 2. Provide clear explanations suitable for both experts and general audiences
42
+ 3. Include relevant context about Fed mandate (price stability, maximum employment)
43
+ 4. Explain implications for markets, businesses, consumers, and the broader economy
44
+ 5. Reference specific meeting dates and decisions when relevant
45
+ 6. Acknowledge uncertainty when data is incomplete or when making forward-looking statements
46
+
47
+ KNOWLEDGE AREAS:
48
+ - FOMC meeting minutes and rate decisions
49
+ - Fed tools: federal funds rate, quantitative easing, forward guidance
50
+ - Economic indicators: inflation, employment, GDP growth, financial conditions
51
+ - Market impacts: bond yields, stock markets, dollar strength, lending conditions
52
+ - Historical context: comparing current policy to past cycles
53
+ - Fed communication strategy and market interpretation
54
+
55
+ RESPONSE STRUCTURE:
56
+ - Start with a direct answer to the user's question
57
+ - Provide supporting context from Fed sources
58
+ - Explain broader implications and connections
59
+ - End with actionable insights or key takeaways
60
+
61
+ TONE: Professional, knowledgeable, and accessible. Avoid jargon without explanation.
62
+
63
+ Available Fed Data Context: {fed_data_context}
64
+
65
+ User Question: {user_question}
66
+ The date is {date}
67
+
68
+ fed_orchestrator: |
69
+ You are a Federal Reserve Tool Orchestrator. Your job is to analyze user queries about Fed policy and FOMC meetings, then decide which tools to use to gather the most relevant information.
70
+
71
+ AVAILABLE TOOLS:
72
+ 1. search_meetings(query: str, limit: int = 3) - Search across all FOMC meeting fields for relevant information
73
+ 2. get_latest_meeting() - Get the most recent FOMC meeting data
74
+ 3. get_rate_decision(date: str) - Get specific meeting data by date (YYYY-MM-DD format)
75
+ 4. compare_meetings(date1: str, date2: str) - Compare two meetings side by side
76
+
77
+ INSTRUCTIONS:
78
+ - Analyze the user query to determine which tools would provide the most relevant information
79
+ - You can use multiple tools if needed to fully answer the question
80
+ - For search queries, extract key terms and use search_meetings
81
+ - For recent/latest questions, use get_latest_meeting
82
+ - For specific date questions, use get_rate_decision
83
+ - For comparison questions, use compare_meetings
84
+ - Always provide the exact function calls in JSON format
85
+
86
+ RESPONSE FORMAT:
87
+ Return a JSON object with this structure:
88
+ {{
89
+ "tools_needed": [
90
+ {{
91
+ "function": "function_name",
92
+ "parameters": {{"param1": "value1", "param2": "value2"}},
93
+ "reasoning": "Why this tool is needed"
94
+ }}
95
+ ],
96
+ "query_analysis": "Brief analysis of what the user is asking for"
97
+ }}
98
+
99
+ EXAMPLES:
100
+ User: "What was the latest rate decision?"
101
+ Response: {{"tools_needed": [{{"function": "get_latest_meeting", "parameters": {{}}, "reasoning": "User wants the most recent FOMC meeting information"}}], "query_analysis": "User is asking for the most recent Fed rate decision"}}
102
+
103
+ User: "Tell me about inflation expectations in recent meetings"
104
+ Response: {{"tools_needed": [{{"function": "search_meetings", "parameters": {{"query": "inflation expectations", "limit": 3}}, "reasoning": "Need to search for inflation-related content across meetings"}}], "query_analysis": "User wants information about inflation expectations from FOMC meetings"}}
105
+
106
+ User Query: {user_query}
107
+ The date is {date}
src/app.py CHANGED
@@ -4,9 +4,13 @@ from datetime import datetime
4
  from typing import List, Dict, Any
5
  import random
6
  import os
 
7
  from dotenv import load_dotenv
8
  from pathlib import Path
9
  from src.modules.fed_tools import search_meetings, get_rate_decision, compare_meetings, get_latest_meeting
 
 
 
10
 
11
  load_dotenv()
12
  _FILE_PATH = Path(__file__).parents[1]
@@ -60,6 +64,48 @@ def load_processed_meetings():
60
  # Load the processed meetings
61
  FOMC_MEETINGS = load_processed_meetings()
62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  def process_fed_query(user_message: str, selected_model: str = "") -> Dict[str, Any]:
64
  """Process user queries using Fed AI tools"""
65
  message_lower = user_message.lower()
@@ -196,38 +242,23 @@ def format_response_with_reasoning(function_result: Dict[str, Any], model_name:
196
  """
197
  return response
198
 
199
- def respond_for_chat_interface(
200
- message: str,
201
- history: list[tuple[str, str]],
202
- api_key: str = "",
203
- ):
204
  """Enhanced response function for gr.ChatInterface with Fed AI Savant capabilities"""
205
 
206
- if not message.strip():
207
- yield "Please enter a question about Federal Reserve policy or FOMC meetings."
208
- return
209
 
210
- if not api_key.strip():
211
- yield "❌ Please enter your AI API key in the configuration panel to use the Fed AI Savant."
212
- return
 
 
 
 
213
 
214
- # Fixed model for Fed AI Savant
215
- model_name = "OAI OSS 120B"
216
-
217
- # Process Fed query using real Fed tools
218
- function_result = process_fed_query(message, model_name)
219
-
220
- # Format response with reasoning chain
221
- formatted_response = format_response_with_reasoning(function_result, model_name)
222
-
223
- # Simulate streaming response
224
- response = ""
225
- for char in formatted_response:
226
- response += char
227
- yield response
228
- # Small delay to simulate streaming
229
- import time
230
- time.sleep(0.01)
231
 
232
  def get_fomc_meetings_sidebar():
233
  """Generate sidebar content with FOMC meeting details"""
@@ -428,14 +459,15 @@ with gr.Blocks(css=custom_css, title="Fed AI Savant", theme=gr.themes.Soft()) as
428
 
429
  chat_interface = gr.ChatInterface(
430
  fn=respond_for_chat_interface,
431
- chatbot=gr.Chatbot(height=200, show_label=False),
 
432
  textbox=gr.Textbox(placeholder="Ask about Fed policy, rate decisions, or FOMC meetings...", scale=10),
433
  examples=[
434
- "What was the rate decision in the last FOMC meeting?"
435
  "Compare June 2024 vs July 2024 FOMC meetings",
436
  "Tell me about inflation expectations",
437
  "Has the Fed's employment stance changed?",
438
- "What was the rate decision in the last FOMC meeting?",
439
  ],
440
  submit_btn="Send",
441
  )
 
4
  from typing import List, Dict, Any
5
  import random
6
  import os
7
+ import yaml
8
  from dotenv import load_dotenv
9
  from pathlib import Path
10
  from src.modules.fed_tools import search_meetings, get_rate_decision, compare_meetings, get_latest_meeting
11
+ from src.modules.llm_completions import get_llm, stream_fed_agent_response
12
+ from gradio import ChatMessage
13
+ import time
14
 
15
  load_dotenv()
16
  _FILE_PATH = Path(__file__).parents[1]
 
64
  # Load the processed meetings
65
  FOMC_MEETINGS = load_processed_meetings()
66
 
67
+ def load_prompt_library():
68
+ """Load prompts from the YAML library"""
69
+ try:
70
+ prompt_file = _FILE_PATH / "configs" / "prompt_library.yaml"
71
+ with open(prompt_file, 'r', encoding='utf-8') as f:
72
+ return yaml.safe_load(f)
73
+ except Exception as e:
74
+ print(f"Error loading prompt library: {e}")
75
+ return {}
76
+
77
+ # Load prompt library
78
+ PROMPT_LIBRARY = load_prompt_library()
79
+
80
+ def get_fed_context_for_query(user_message: str) -> str:
81
+ """Get relevant Fed data context for the user's query"""
82
+ message_lower = user_message.lower()
83
+
84
+ # Get relevant meeting data based on query type
85
+ if 'latest' in message_lower or 'most recent' in message_lower:
86
+ result = get_latest_meeting()
87
+ if result["success"]:
88
+ meeting = result["meeting"]
89
+ return f"Latest FOMC Meeting ({meeting.get('date', 'unknown')}): {meeting.get('forward_guidance', '')[:300]}..."
90
+
91
+ elif any(word in message_lower for word in ['search', 'find', 'about']):
92
+ search_query = user_message.replace('search for', '').replace('find', '').replace('about', '').strip()
93
+ result = search_meetings(search_query, limit=2)
94
+ if result["success"] and result["count"] > 0:
95
+ context = f"Relevant FOMC meetings for '{search_query}':\n"
96
+ for meeting in result["results"][:2]:
97
+ context += f"- {meeting.get('date', 'unknown')}: {meeting.get('forward_guidance', '')[:200]}...\n"
98
+ return context
99
+
100
+ # Default: return latest meeting info
101
+ result = get_latest_meeting()
102
+ if result["success"]:
103
+ meeting = result["meeting"]
104
+ return f"Current Fed Policy Context: Rate at {meeting.get('rate', 'unknown')}, {meeting.get('action', 'maintained')} in latest meeting ({meeting.get('date', 'unknown')})"
105
+
106
+ return "Fed data context not available. Please ensure the data pipeline has been run."
107
+
108
+
109
  def process_fed_query(user_message: str, selected_model: str = "") -> Dict[str, Any]:
110
  """Process user queries using Fed AI tools"""
111
  message_lower = user_message.lower()
 
242
  """
243
  return response
244
 
245
+ def respond_for_chat_interface(message: str, history):
 
 
 
 
246
  """Enhanced response function for gr.ChatInterface with Fed AI Savant capabilities"""
247
 
248
+ # Get API key from environment or return error
249
+ api_key = os.getenv("FIREWORKS_API_KEY", "")
 
250
 
251
+ # Create Fed tools dictionary
252
+ fed_tools = {
253
+ "search_meetings": search_meetings,
254
+ "get_latest_meeting": get_latest_meeting,
255
+ "get_rate_decision": get_rate_decision,
256
+ "compare_meetings": compare_meetings
257
+ }
258
 
259
+ # Use the new orchestrator function
260
+ for messages in stream_fed_agent_response(message, api_key, PROMPT_LIBRARY, fed_tools):
261
+ yield messages
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
 
263
  def get_fomc_meetings_sidebar():
264
  """Generate sidebar content with FOMC meeting details"""
 
459
 
460
  chat_interface = gr.ChatInterface(
461
  fn=respond_for_chat_interface,
462
+ type="messages",
463
+ chatbot=gr.Chatbot(height=500, show_label=False),
464
  textbox=gr.Textbox(placeholder="Ask about Fed policy, rate decisions, or FOMC meetings...", scale=10),
465
  examples=[
466
+ "What was the rate decision in the last FOMC meeting?",
467
  "Compare June 2024 vs July 2024 FOMC meetings",
468
  "Tell me about inflation expectations",
469
  "Has the Fed's employment stance changed?",
470
+ "What factors influenced the latest rate decision?",
471
  ],
472
  submit_btn="Send",
473
  )
src/modules/fed_tools.py CHANGED
@@ -13,7 +13,6 @@ def _load_meetings_data() -> List[Dict[str, Any]]:
13
  if MEETINGS_FILE.exists():
14
  with open(MEETINGS_FILE, 'r', encoding='utf-8') as f:
15
  data = json.load(f)
16
- # Sort meetings by date (newest first)
17
  return sorted(data, key=lambda x: x.get('date', ''), reverse=True)
18
  else:
19
  return []
@@ -53,7 +52,6 @@ def search_meetings(query: str, limit: int = 3) -> Dict[str, Any]:
53
  score = 0
54
  matched_fields = []
55
 
56
- # Search in various fields and assign scores based on relevance
57
  search_fields = {
58
  'date': 2,
59
  'title': 1,
@@ -82,7 +80,6 @@ def search_meetings(query: str, limit: int = 3) -> Dict[str, Any]:
82
  'matched_fields': matched_fields
83
  })
84
 
85
- # Sort by score (highest first) and limit results
86
  scored_meetings.sort(key=lambda x: x['score'], reverse=True)
87
  top_results = scored_meetings[:limit]
88
 
@@ -117,14 +114,12 @@ def get_rate_decision(date: str) -> Dict[str, Any]:
117
  "error": "No meetings data available"
118
  }
119
 
120
- # Find meeting by exact date match
121
  target_meeting = None
122
  for meeting in meetings_data:
123
  if meeting.get('date') == date:
124
  target_meeting = meeting
125
  break
126
 
127
- # If no exact match, try to find closest date within 30 days
128
  if not target_meeting and date:
129
  try:
130
  target_date = datetime.strptime(date, '%Y-%m-%d')
 
13
  if MEETINGS_FILE.exists():
14
  with open(MEETINGS_FILE, 'r', encoding='utf-8') as f:
15
  data = json.load(f)
 
16
  return sorted(data, key=lambda x: x.get('date', ''), reverse=True)
17
  else:
18
  return []
 
52
  score = 0
53
  matched_fields = []
54
 
 
55
  search_fields = {
56
  'date': 2,
57
  'title': 1,
 
80
  'matched_fields': matched_fields
81
  })
82
 
 
83
  scored_meetings.sort(key=lambda x: x['score'], reverse=True)
84
  top_results = scored_meetings[:limit]
85
 
 
114
  "error": "No meetings data available"
115
  }
116
 
 
117
  target_meeting = None
118
  for meeting in meetings_data:
119
  if meeting.get('date') == date:
120
  target_meeting = meeting
121
  break
122
 
 
123
  if not target_meeting and date:
124
  try:
125
  target_date = datetime.strptime(date, '%Y-%m-%d')
src/modules/llm_completions.py CHANGED
@@ -1,12 +1,18 @@
1
  from fireworks import LLM
2
  from pydantic import BaseModel
3
  import asyncio
 
 
 
 
4
 
5
  MODELS = {
6
  "small": "accounts/fireworks/models/qwen3-235b-a22b-instruct-2507",
7
  "large": "accounts/fireworks/models/kimi-k2-instruct"
8
  }
9
 
 
 
10
  semaphore = asyncio.Semaphore(10)
11
 
12
  def get_llm(model: str, api_key: str) -> LLM:
@@ -39,6 +45,44 @@ async def get_llm_completion(llm: LLM, prompt_text: str, output_class: BaseModel
39
  )
40
 
41
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  async def run_multi_llm_completions(llm: LLM, prompts: list[str], output_class: BaseModel) -> list[str]:
43
  """
44
  Run multiple LLM completions in parallel
@@ -65,3 +109,190 @@ async def run_multi_llm_completions(llm: LLM, prompts: list[str], output_class:
65
  ]
66
  return await asyncio.gather(*tasks)
67
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from fireworks import LLM
2
  from pydantic import BaseModel
3
  import asyncio
4
+ import json
5
+ import time
6
+ from typing import Dict, Any, List
7
+ from gradio import ChatMessage
8
 
9
  MODELS = {
10
  "small": "accounts/fireworks/models/qwen3-235b-a22b-instruct-2507",
11
  "large": "accounts/fireworks/models/kimi-k2-instruct"
12
  }
13
 
14
+ TODAY = time.strftime("%Y-%m-%d")
15
+
16
  semaphore = asyncio.Semaphore(10)
17
 
18
  def get_llm(model: str, api_key: str) -> LLM:
 
45
  )
46
 
47
 
48
+ async def get_streaming_completion(llm: LLM, prompt_text: str, system_prompt: str = None):
49
+ """
50
+ Get streaming completion from LLM for real-time responses
51
+
52
+ :param llm: The LLM instance
53
+ :param prompt_text: The user's input message
54
+ :param system_prompt: Optional system prompt for context
55
+ :return: Generator yielding response chunks
56
+ """
57
+ messages = []
58
+
59
+ if system_prompt:
60
+ messages.append({
61
+ "role": "system",
62
+ "content": system_prompt
63
+ })
64
+
65
+ messages.append({
66
+ "role": "user",
67
+ "content": prompt_text
68
+ })
69
+
70
+ try:
71
+ response = llm.chat.completions.create(
72
+ messages=messages,
73
+ temperature=0.2,
74
+ stream=True,
75
+ max_tokens=1000
76
+ )
77
+
78
+ for chunk in response:
79
+ if chunk.choices[0].delta.content:
80
+ yield chunk.choices[0].delta.content
81
+
82
+ except Exception as e:
83
+ yield f"Error generating response: {str(e)}"
84
+
85
+
86
  async def run_multi_llm_completions(llm: LLM, prompts: list[str], output_class: BaseModel) -> list[str]:
87
  """
88
  Run multiple LLM completions in parallel
 
109
  ]
110
  return await asyncio.gather(*tasks)
111
 
112
+ def get_orchestrator_decision(user_query: str, api_key: str, prompt_library: Dict[str, str]) -> Dict[str, Any]:
113
+ """Use orchestrator LLM to decide which tools to use"""
114
+ try:
115
+ orchestrator_prompt = prompt_library.get('fed_orchestrator', '')
116
+ formatted_prompt = orchestrator_prompt.format(user_query=user_query, date=TODAY)
117
+
118
+ llm = get_llm("large", api_key)
119
+
120
+ response = llm.chat.completions.create(
121
+ messages=[
122
+ {"role": "system", "content": "You are a tool orchestrator. Always respond with valid JSON."},
123
+ {"role": "user", "content": formatted_prompt}
124
+ ],
125
+ temperature=0.1,
126
+ max_tokens=500
127
+ )
128
+
129
+ # Parse JSON response
130
+ result = json.loads(response.choices[0].message.content)
131
+ return {"success": True, "decision": result}
132
+
133
+ except Exception as e:
134
+ print(f"Error in orchestrator: {e}")
135
+ # Fallback to simple logic
136
+ return {
137
+ "success": False,
138
+ "decision": {
139
+ "tools_needed": [{"function": "get_latest_meeting", "parameters": {}, "reasoning": "Fallback to latest meeting"}],
140
+ "query_analysis": f"Error occurred, using fallback for: {user_query}"
141
+ }
142
+ }
143
+
144
+ def execute_fed_tools(tools_decision: Dict[str, Any], fed_tools: Dict[str, callable]) -> List[Dict[str, Any]]:
145
+ """Execute the tools determined by the orchestrator"""
146
+ results = []
147
+
148
+ for tool in tools_decision.get("tools_needed", []):
149
+ function_name = tool.get("function", "")
150
+ parameters = tool.get("parameters", {})
151
+ reasoning = tool.get("reasoning", "")
152
+
153
+ start_time = time.time()
154
+
155
+ try:
156
+ # Execute the appropriate function
157
+ if function_name in fed_tools:
158
+ tool_func = fed_tools[function_name]
159
+ result = tool_func(**parameters)
160
+ else:
161
+ result = {"success": False, "error": f"Unknown function: {function_name}"}
162
+
163
+ execution_time = time.time() - start_time
164
+
165
+ results.append({
166
+ "function": function_name,
167
+ "parameters": parameters,
168
+ "reasoning": reasoning,
169
+ "result": result,
170
+ "execution_time": execution_time,
171
+ "success": result.get("success", False)
172
+ })
173
+
174
+ except Exception as e:
175
+ execution_time = time.time() - start_time
176
+ results.append({
177
+ "function": function_name,
178
+ "parameters": parameters,
179
+ "reasoning": reasoning,
180
+ "result": {"success": False, "error": str(e)},
181
+ "execution_time": execution_time,
182
+ "success": False
183
+ })
184
+
185
+ return results
186
+
187
+ def stream_fed_agent_response(
188
+ message: str,
189
+ api_key: str,
190
+ prompt_library: Dict[str, str],
191
+ fed_tools: Dict[str, callable]
192
+ ):
193
+ """Main orchestrator function that coordinates tools and generates responses with ChatMessage objects"""
194
+
195
+ if not message.strip():
196
+ yield [ChatMessage(role="assistant", content="Please enter a question about Federal Reserve policy or FOMC meetings.")]
197
+ return
198
+
199
+ if not api_key.strip():
200
+ yield [ChatMessage(role="assistant", content="❌ Please set your FIREWORKS_API_KEY environment variable.")]
201
+ return
202
+
203
+ messages = []
204
+
205
+ try:
206
+ # Step 1: Use orchestrator to determine tools needed
207
+ messages.append(ChatMessage(
208
+ role="assistant",
209
+ content="Analyzing your query...",
210
+ metadata={"title": "🧠 Planning", "status": "pending"}
211
+ ))
212
+ yield messages
213
+
214
+ orchestrator_result = get_orchestrator_decision(message, api_key, prompt_library)
215
+ tools_decision = orchestrator_result["decision"]
216
+
217
+ # Update planning message
218
+ messages[0] = ChatMessage(
219
+ role="assistant",
220
+ content=f"Query Analysis: {tools_decision.get('query_analysis', 'Analyzing Fed data requirements')}\n\nTools needed: {len(tools_decision.get('tools_needed', []))}",
221
+ metadata={"title": "🧠 Planning", "status": "done"}
222
+ )
223
+ yield messages
224
+
225
+ # Step 2: Execute the determined tools
226
+ if tools_decision.get("tools_needed"):
227
+ for i, tool in enumerate(tools_decision["tools_needed"]):
228
+ tool_msg = ChatMessage(
229
+ role="assistant",
230
+ content=f"Executing: {tool['function']}({', '.join([f'{k}={v}' for k, v in tool['parameters'].items()])})\n\nReasoning: {tool['reasoning']}",
231
+ metadata={"title": f"πŸ”§ Tool {i+1}: {tool['function']}", "status": "pending"}
232
+ )
233
+ messages.append(tool_msg)
234
+ yield messages
235
+
236
+ # Execute all tools
237
+ tool_results = execute_fed_tools(tools_decision, fed_tools)
238
+
239
+ # Update tool messages with results
240
+ for i, (tool_result, tool_msg) in enumerate(zip(tool_results, messages[1:])):
241
+ execution_time = tool_result["execution_time"]
242
+ success_status = "βœ…" if tool_result["success"] else "❌"
243
+
244
+ messages[i+1] = ChatMessage(
245
+ role="assistant",
246
+ content=f"{success_status} {tool_result['function']} completed\n\nExecution time: {execution_time:.2f}s\n\nResult summary: {str(tool_result['result'])[:200]}...",
247
+ metadata={"title": f"πŸ”§ Tool {i+1}: {tool_result['function']}", "status": "done", "duration": execution_time}
248
+ )
249
+
250
+ yield messages
251
+
252
+ # Step 3: Use results to generate final response
253
+ combined_context = ""
254
+ for result in tool_results:
255
+ if result["success"]:
256
+ combined_context += f"\n\nFrom {result['function']}: {json.dumps(result['result'], indent=2)}"
257
+
258
+ # Generate Fed Savant response using tool results
259
+ system_prompt_template = prompt_library.get('fed_savant_chat', '')
260
+ system_prompt = system_prompt_template.format(
261
+ fed_data_context=combined_context,
262
+ user_question=message,
263
+ date=TODAY
264
+ )
265
+
266
+ # Initialize LLM and get streaming response
267
+ llm = get_llm("large", api_key)
268
+
269
+ final_response = ""
270
+ for chunk in llm.chat.completions.create(
271
+ messages=[
272
+ {"role": "system", "content": system_prompt},
273
+ {"role": "user", "content": message}
274
+ ],
275
+ temperature=0.2,
276
+ stream=True,
277
+ max_tokens=1000
278
+ ):
279
+ if chunk.choices[0].delta.content:
280
+ final_response += chunk.choices[0].delta.content
281
+
282
+ # Update messages list with current response
283
+ if len(messages) > len(tool_results):
284
+ messages[-1] = ChatMessage(role="assistant", content=final_response)
285
+ else:
286
+ messages.append(ChatMessage(role="assistant", content=final_response))
287
+
288
+ yield messages
289
+
290
+ else:
291
+ # No tools needed, direct response
292
+ messages.append(ChatMessage(role="assistant", content="No specific tools required. Providing general Fed information."))
293
+ yield messages
294
+
295
+ except Exception as e:
296
+ messages.append(ChatMessage(role="assistant", content=f"Error generating response: {str(e)}"))
297
+ yield messages
298
+