TuringsSolutions commited on
Commit
dbbd401
Β·
verified Β·
1 Parent(s): b38976a

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +516 -0
app.py ADDED
@@ -0,0 +1,516 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ==============================================================================
2
+ # Tool World: Advanced Prototype (Hugging Face Space Version)
3
+ # ==============================================================================
4
+ #
5
+ # This script has been updated to run as a Hugging Face Space.
6
+ #
7
+ # Key Upgrades from the original script:
8
+ # 1. **Hugging Face Model Integration**: Uses the 'google/gemma-3n-E4B' model
9
+ # from the Hugging Face Hub for argument extraction.
10
+ # 2. **Environment Variable Management**: Securely accesses the
11
+ # HUGGING_FACE_HUB_TOKEN using os.environ.get(), which is the standard
12
+ # for Hugging Face Spaces.
13
+ # 3. **Standard Dependencies**: All dependencies are managed via a
14
+ # `requirements.txt` file.
15
+ #
16
+ # ==============================================================================
17
+
18
+ # ------------------------------
19
+ # 1. INSTALL & IMPORT PACKAGES
20
+ # ------------------------------
21
+ import numpy as np
22
+ import umap
23
+ import gradio as gr
24
+ from sentence_transformers import SentenceTransformer, util
25
+ import matplotlib.pyplot as plt
26
+ import json
27
+ import os
28
+ from datetime import datetime, timedelta
29
+ import torch
30
+ from transformers import AutoTokenizer, AutoModelForCausalLM
31
+
32
+ # ------------------------------
33
+ # 2. CONFIGURE & LOAD MODELS
34
+ # ------------------------------
35
+
36
+ print("βš™οΈ Loading embedding model...")
37
+ # Using a powerful model for better semantic understanding
38
+ embedder = SentenceTransformer('all-mpnet-base-v2')
39
+ print("βœ… Embedding model loaded.")
40
+
41
+ # --- Configuration for Hugging Face Model-based Argument Extraction ---
42
+ try:
43
+ HF_TOKEN = os.environ.get('HUGGING_FACE_HUB_TOKEN')
44
+ if HF_TOKEN is None:
45
+ raise ValueError("HUGGING_FACE_HUB_TOKEN secret not found.")
46
+
47
+ print("βš™οΈ Loading Hugging Face model for argument extraction...")
48
+ # Using the user-specified Gemma model
49
+ model_id = "google/gemma-3n-E4B"
50
+
51
+ hf_tokenizer = AutoTokenizer.from_pretrained(model_id, token=HF_TOKEN)
52
+
53
+ # --------------------------------------------------------------------------
54
+ # βœ… FIX: Manually set the chat template for the Gemma model.
55
+ # This is required because the specified model does not have a default
56
+ # template set in its tokenizer config on the Hugging Face Hub.
57
+ # --------------------------------------------------------------------------
58
+ gemma_template = (
59
+ "{% for message in messages %}"
60
+ "{{'<start_of_turn>' + message['role'] + '\n' + message['content'] + '<end_of_turn>\n'}}"
61
+ "{% endfor %}"
62
+ "{% if add_generation_prompt %}"
63
+ "{{ '<start_of_turn>model\n' }}"
64
+ "{% endif %}"
65
+ )
66
+ hf_tokenizer.chat_template = gemma_template
67
+ # --------------------------------------------------------------------------
68
+
69
+ hf_model = AutoModelForCausalLM.from_pretrained(
70
+ model_id,
71
+ token=HF_TOKEN,
72
+ torch_dtype=torch.bfloat16, # Use bfloat16 for efficiency
73
+ device_map="auto" # Automatically use GPU if available
74
+ )
75
+ USE_HF_LLM = True
76
+ print(f"βœ… Successfully loaded '{model_id}' model and set chat template.")
77
+
78
+ except Exception as e:
79
+ USE_HF_LLM = False
80
+ print(f"⚠️ WARNING: Could not load the Hugging Face model. Reason: {e}")
81
+ print(" Argument extraction will be disabled.")
82
+
83
+
84
+ # ------------------------------
85
+ # 3. ADVANCED TOOL DEFINITION
86
+ # ------------------------------
87
+
88
+ class Tool:
89
+ """
90
+ Represents a tool with structured arguments and rich descriptive data
91
+ for high-quality embedding.
92
+ """
93
+ def __init__(self, name, description, args_schema, function, examples=None):
94
+ self.name = name
95
+ self.description = description
96
+ self.args_schema = args_schema
97
+ self.function = function
98
+ self.examples = examples or []
99
+ self.embedding = self._create_embedding()
100
+
101
+ def _create_embedding(self):
102
+ """
103
+ Creates a rich embedding by combining the tool's name, description,
104
+ argument structure, and examples.
105
+ """
106
+ schema_str = json.dumps(self.args_schema, indent=2)
107
+ examples_str = "\n".join([f" - Example: {ex['prompt']} -> Args: {json.dumps(ex['args'])}" for ex in self.examples])
108
+
109
+ embedding_text = (
110
+ f"Tool Name: {self.name}\n"
111
+ f"Description: {self.description}\n"
112
+ f"Argument Schema: {schema_str}\n"
113
+ f"Usage Examples:\n{examples_str}"
114
+ )
115
+ return embedder.encode(embedding_text, convert_to_tensor=True)
116
+
117
+ def __repr__(self):
118
+ return f"<Tool: {self.name}>"
119
+
120
+ # ------------------------------
121
+ # 4. TOOL IMPLEMENTATIONS
122
+ # ------------------------------
123
+
124
+ def get_weather_forecast(location: str, days: int = 1):
125
+ """Simulates fetching a weather forecast."""
126
+ if not isinstance(location, str) or not isinstance(days, int):
127
+ return {"error": "Invalid argument types. 'location' must be a string and 'days' an integer."}
128
+
129
+ weather_conditions = ["Sunny", "Cloudy", "Rainy", "Windy", "Snowy"]
130
+ response = {"location": location, "forecast": []}
131
+
132
+ for i in range(days):
133
+ date = (datetime.now() + timedelta(days=i)).strftime('%Y-%m-%d')
134
+ condition = np.random.choice(weather_conditions)
135
+ temp = np.random.randint(5, 25)
136
+ response["forecast"].append({
137
+ "date": date,
138
+ "condition": condition,
139
+ "temperature_celsius": temp
140
+ })
141
+ return response
142
+
143
+ def create_calendar_event(title: str, date: str, duration_minutes: int = 60, participants: list = None):
144
+ """Simulates creating a calendar event."""
145
+ try:
146
+ # Check for relative terms like "tomorrow"
147
+ if 'tomorrow' in date.lower():
148
+ event_base_date = datetime.now() + timedelta(days=1)
149
+ # Try to extract time, default to 9am if not specified
150
+ try:
151
+ time_part = datetime.strptime(date, '%I:%M %p').time()
152
+ except ValueError:
153
+ try:
154
+ time_part = datetime.strptime(date, '%H:%M').time()
155
+ except ValueError:
156
+ time_part = datetime.strptime('09:00', '%H:%M').time()
157
+ event_time = event_base_date.replace(hour=time_part.hour, minute=time_part.minute, second=0, microsecond=0)
158
+ else:
159
+ event_time = datetime.strptime(date, '%Y-%m-%d %H:%M')
160
+
161
+ return {
162
+ "status": "success",
163
+ "event_created": {
164
+ "title": title,
165
+ "start_time": event_time.isoformat(),
166
+ "end_time": (event_time + timedelta(minutes=duration_minutes)).isoformat(),
167
+ "participants": participants or ["organizer"]
168
+ }
169
+ }
170
+ except ValueError:
171
+ return {"error": "Invalid date format. Please use 'YYYY-MM-DD HH:MM' or a relative term like 'tomorrow at 10:00'."}
172
+
173
+ def summarize_text(text: str, compression_level: str = 'medium'):
174
+ """Summarizes a given text based on a compression level."""
175
+ word_count = len(text.split())
176
+ ratios = {'high': 0.2, 'medium': 0.4, 'low': 0.7}
177
+ ratio = ratios.get(compression_level, 0.4)
178
+ summary_length = int(word_count * ratio)
179
+ summary = " ".join(text.split()[:summary_length])
180
+ return {"summary": summary + "...", "original_word_count": word_count, "summary_word_count": summary_length}
181
+
182
+ def search_web(query: str, domain: str = None):
183
+ """Simulates a web search, with an optional domain filter."""
184
+ results = [
185
+ f"Simulated result 1 for '{query}'",
186
+ f"Simulated result 2 for '{query}'",
187
+ f"Simulated result 3 for '{query}'"
188
+ ]
189
+ if domain:
190
+ return {"status": f"Searching for '{query}' within '{domain}'...", "results": results}
191
+ return {"status": f"Searching for '{query}'...", "results": results}
192
+
193
+
194
+ # ------------------------------
195
+ # 5. DEFINE THE TOOLSET
196
+ # ------------------------------
197
+
198
+ tools = [
199
+ Tool(
200
+ name="weather_reporter",
201
+ description="Provides the weather forecast for a specific location for a given number of days.",
202
+ args_schema={
203
+ "type": "object",
204
+ "properties": {
205
+ "location": {"type": "string", "description": "The city and state, e.g., 'San Francisco, CA'"},
206
+ "days": {"type": "integer", "description": "The number of days to forecast", "default": 1}
207
+ },
208
+ "required": ["location"]
209
+ },
210
+ function=get_weather_forecast,
211
+ examples=[
212
+ {"prompt": "what's the weather like in London for the next 3 days", "args": {"location": "London", "days": 3}},
213
+ {"prompt": "forecast for New York tomorrow", "args": {"location": "New York", "days": 1}}
214
+ ]
215
+ ),
216
+ Tool(
217
+ name="calendar_creator",
218
+ description="Creates a new event in the user's calendar.",
219
+ args_schema={
220
+ "type": "object",
221
+ "properties": {
222
+ "title": {"type": "string", "description": "The title of the calendar event"},
223
+ "date": {"type": "string", "description": "The start date and time in 'YYYY-MM-DD HH:MM' format. Handles relative terms like 'tomorrow at 10:30 am'."},
224
+ "duration_minutes": {"type": "integer", "description": "The duration of the event in minutes", "default": 60},
225
+ "participants": {"type": "array", "items": {"type": "string"}, "description": "List of email addresses of participants"}
226
+ },
227
+ "required": ["title", "date"]
228
+ },
229
+ function=create_calendar_event,
230
+ examples=[
231
+ {"prompt": "Schedule a 'Project Sync' for tomorrow at 3pm with [email protected]", "args": {"title": "Project Sync", "date": (datetime.now() + timedelta(days=1)).strftime('%Y-%m-%d 15:00'), "participants": ["[email protected]"]}},
232
+ {"prompt": "new event: Dentist appointment on 2025-12-20 at 10:00 for 45 mins", "args": {"title": "Dentist appointment", "date": "2025-12-20 10:00", "duration_minutes": 45}}
233
+ ]
234
+ ),
235
+ Tool(
236
+ name="text_summarizer",
237
+ description="Summarizes a long piece of text. Can be set to high, medium, or low compression.",
238
+ args_schema={
239
+ "type": "object",
240
+ "properties": {
241
+ "text": {"type": "string", "description": "The text to be summarized."},
242
+ "compression_level": {"type": "string", "enum": ["high", "medium", "low"], "description": "The level of summarization.", "default": "medium"}
243
+ },
244
+ "required": ["text"]
245
+ },
246
+ function=summarize_text,
247
+ examples=[
248
+ {"prompt": "summarize this article for me, make it very short: [long text...]", "args": {"text": "[long text...]", "compression_level": "high"}}
249
+ ]
250
+ ),
251
+ Tool(
252
+ name="web_search",
253
+ description="Performs a web search to find information on a topic.",
254
+ args_schema={
255
+ "type": "object",
256
+ "properties": {
257
+ "query": {"type": "string", "description": "The search query."},
258
+ "domain": {"type": "string", "description": "Optional: a specific website domain to search within (e.g., 'wikipedia.org')."}
259
+ },
260
+ "required": ["query"]
261
+ },
262
+ function=search_web,
263
+ examples=[
264
+ {"prompt": "who invented the light bulb", "args": {"query": "who invented the light bulb"}},
265
+ {"prompt": "search for 'transformer models' on arxiv.org", "args": {"query": "transformer models", "domain": "arxiv.org"}}
266
+ ]
267
+ )
268
+ ]
269
+
270
+ print(f"βœ… {len(tools)} tools defined and embedded.")
271
+
272
+ # ------------------------------
273
+ # 6. CORE LOGIC: TOOL SELECTION & ARGUMENT EXTRACTION
274
+ # ------------------------------
275
+
276
+ def find_best_tool(user_intent: str):
277
+ """Finds the most semantically similar tool for a user's intent."""
278
+ intent_embedding = embedder.encode(user_intent, convert_to_tensor=True)
279
+ # Move tool embeddings to the same device as the intent embedding
280
+ tool_embeddings = [tool.embedding.to(intent_embedding.device) for tool in tools]
281
+ similarities = [util.pytorch_cos_sim(intent_embedding, tool_emb).item() for tool_emb in tool_embeddings]
282
+ best_index = int(np.argmax(similarities))
283
+ best_tool = tools[best_index]
284
+ best_score = similarities[best_index]
285
+ return best_tool, best_score, similarities
286
+
287
+ def extract_arguments_hf(user_prompt: str, tool: Tool):
288
+ """
289
+ Uses a local Hugging Face model to extract structured arguments.
290
+ """
291
+ system_prompt = f"""
292
+ You are an expert at extracting structured data from natural language.
293
+ Your task is to analyze the user's prompt and extract the arguments required to call the tool: '{tool.name}'.
294
+
295
+ You must adhere to the following JSON schema for the arguments:
296
+ {json.dumps(tool.args_schema, indent=2)}
297
+
298
+ - If a value is not present in the prompt for a non-required field, omit it from the JSON.
299
+ - If a required value is missing, return a JSON object with an "error" key explaining what is missing.
300
+ - Today's date is {datetime.now().strftime('%Y-%m-%d')}. If the user says "tomorrow", use {(datetime.now() + timedelta(days=1)).strftime('%Y-%m-%d')}.
301
+ - Respond ONLY with a valid JSON object. Do not include any other text, explanation, or markdown code blocks.
302
+ """
303
+
304
+ # Gemma instruction-following format
305
+ chat = [
306
+ # Gemma does not use a 'system' role. Instructions are part of the first user message.
307
+ {"role": "user", "content": f"{system_prompt}\n\nUser Prompt: \"{user_prompt}\""},
308
+ ]
309
+
310
+ prompt = hf_tokenizer.apply_chat_template(chat, tokenize=False, add_generation_prompt=True)
311
+
312
+ try:
313
+ inputs = hf_tokenizer.encode(prompt, add_special_tokens=False, return_tensors="pt").to(hf_model.device)
314
+
315
+ # Generate with the model
316
+ outputs = hf_model.generate(input_ids=inputs, max_new_tokens=256, do_sample=False)
317
+ decoded_output = hf_tokenizer.decode(outputs[0][len(inputs[0]):], skip_special_tokens=True)
318
+
319
+ # Clean the response to find the JSON object
320
+ json_str = decoded_output.strip()
321
+
322
+ # Find the first '{' and the last '}' to get the JSON part
323
+ json_start = json_str.find('{')
324
+ json_end = json_str.rfind('}')
325
+
326
+ if json_start != -1 and json_end != -1:
327
+ json_str = json_str[json_start : json_end + 1]
328
+ return json.loads(json_str)
329
+ else:
330
+ raise json.JSONDecodeError("No JSON object found in the model output.", json_str, 0)
331
+
332
+ except Exception as e:
333
+ print(f"Error during HF model inference or JSON parsing: {e}")
334
+ return {"error": f"Failed to extract arguments with the local LLM. Details: {str(e)}"}
335
+
336
+ def execute_tool(user_prompt: str):
337
+ """The main pipeline: Find tool, extract args, execute."""
338
+ selected_tool, score, _ = find_best_tool(user_prompt)
339
+
340
+ if USE_HF_LLM:
341
+ print(f"βš™οΈ Selected Tool: {selected_tool.name}. Extracting arguments with Gemma...")
342
+ extracted_args = extract_arguments_hf(user_prompt, selected_tool)
343
+ else:
344
+ # Fallback if the model failed to load
345
+ extracted_args = {"error": "Argument extraction is disabled because the Hugging Face model could not be loaded."}
346
+
347
+ if 'error' in extracted_args:
348
+ print(f"❌ Argument extraction failed: {extracted_args['error']}")
349
+ # Ensure the final output string is valid JSON
350
+ final_output_str = json.dumps({
351
+ "error": "Execution failed during argument extraction.",
352
+ "details": extracted_args.get('error', 'Unknown extraction error')
353
+ })
354
+ return (
355
+ user_prompt,
356
+ selected_tool.name,
357
+ f"{score:.3f}",
358
+ json.dumps(extracted_args, indent=2),
359
+ final_output_str
360
+ )
361
+
362
+ print(f"βœ… Arguments extracted: {json.dumps(extracted_args, indent=2)}")
363
+
364
+ try:
365
+ print(f"πŸš€ Executing tool function: {selected_tool.name}...")
366
+ output = selected_tool.function(**extracted_args)
367
+ print(f"βœ… Execution complete.")
368
+ output_str = json.dumps(output, indent=2)
369
+ except Exception as e:
370
+ print(f"❌ Tool execution failed: {e}")
371
+ output_str = f'{{"error": "Tool execution failed", "details": "{str(e)}"}}'
372
+
373
+ return (
374
+ user_prompt,
375
+ selected_tool.name,
376
+ f"{score:.3f}",
377
+ json.dumps(extracted_args, indent=2),
378
+ output_str
379
+ )
380
+
381
+
382
+ # ------------------------------
383
+ # 7. VISUALIZATION
384
+ # ------------------------------
385
+
386
+ def plot_tool_world(user_intent=None):
387
+ """Generates a 2D UMAP plot of the tool latent space."""
388
+ tool_vectors = [tool.embedding.cpu().numpy() for tool in tools]
389
+ labels = [tool.name for tool in tools]
390
+ all_vectors = tool_vectors
391
+
392
+ if user_intent and user_intent.strip():
393
+ intent_vector = embedder.encode(user_intent, convert_to_tensor=True).cpu().numpy()
394
+ all_vectors.append(intent_vector)
395
+ labels.append("Your Intent")
396
+
397
+ # UMAP requires at least 2 neighbors
398
+ n_neighbors = min(len(all_vectors) - 1, 5)
399
+ if n_neighbors < 1:
400
+ n_neighbors = 1
401
+
402
+ reducer = umap.UMAP(n_neighbors=n_neighbors, min_dist=0.3, metric='cosine', random_state=42)
403
+
404
+ # UMAP fit_transform requires at least 2 samples
405
+ if len(all_vectors) < 2:
406
+ # Create a dummy plot if there's not enough data
407
+ fig, ax = plt.subplots(figsize=(10, 7))
408
+ ax.text(0.5, 0.5, "Not enough data to create a plot.", ha='center', va='center')
409
+ return fig
410
+
411
+ reduced_vectors = reducer.fit_transform(all_vectors)
412
+
413
+ plt.style.use('seaborn-v0_8-whitegrid')
414
+ fig, ax = plt.subplots(figsize=(10, 7))
415
+
416
+ for i, label in enumerate(labels):
417
+ x, y = reduced_vectors[i]
418
+ if label == "Your Intent":
419
+ ax.scatter(x, y, color='red', s=150, zorder=5, label=label, marker='*')
420
+ ax.text(x, y + 0.05, label, fontsize=12, ha='center', color='red', weight='bold')
421
+ else:
422
+ ax.scatter(x, y, s=100, alpha=0.8, label=label)
423
+ ax.text(x, y + 0.05, label, fontsize=10, ha='center')
424
+
425
+ ax.set_title("Tool World: Latent Space Map", fontsize=16)
426
+ ax.set_xlabel("UMAP Dimension 1", fontsize=12)
427
+ ax.set_ylabel("UMAP Dimension 2", fontsize=12)
428
+ ax.grid(True)
429
+
430
+ handles, labels_legend = ax.get_legend_handles_labels()
431
+ by_label = dict(zip(labels_legend, handles))
432
+ ax.legend(by_label.values(), by_label.keys())
433
+
434
+ plt.tight_layout()
435
+ return fig
436
+
437
+
438
+ # ------------------------------
439
+ # 8. GRADIO INTERFACE
440
+ # ------------------------------
441
+
442
+ print("πŸš€ Launching Gradio interface...")
443
+
444
+ with gr.Blocks(theme=gr.themes.Soft()) as demo:
445
+ gr.Markdown("# πŸ› οΈ Tool World: Advanced Prototype (Hugging Face Version)")
446
+ gr.Markdown(
447
+ "Enter a natural language command. The system will select the best tool, "
448
+ "extract structured arguments with **google/gemma-3n-E4B**, and execute it."
449
+ )
450
+
451
+ with gr.Row():
452
+ with gr.Column(scale=1):
453
+ inp = gr.Textbox(
454
+ label="Your Intent",
455
+ placeholder="e.g., What's the weather in Paris for 2 days?",
456
+ lines=3
457
+ )
458
+ run_btn = gr.Button("Invoke Tool", variant="primary")
459
+
460
+ gr.Markdown("---")
461
+ gr.Markdown("### Examples")
462
+ gr.Examples(
463
+ examples=[
464
+ "Schedule a 'Team Meeting' for tomorrow at 10:30 am",
465
+ "What is the weather forecast in Tokyo for the next 5 days?",
466
+ "search for the latest news on generative AI on reuters.com",
467
+ "Please give me a very short summary of this text: The Industrial Revolution was the transition to new manufacturing processes in Europe and the United States, in the period from about 1760 to sometime between 1820 and 1840."
468
+ ],
469
+ inputs=inp
470
+ )
471
+
472
+ with gr.Column(scale=2):
473
+ gr.Markdown("### Invocation Details")
474
+ with gr.Row():
475
+ out_tool = gr.Textbox(label="Selected Tool", interactive=False)
476
+ out_score = gr.Textbox(label="Similarity Score", interactive=False)
477
+
478
+ out_args = gr.JSON(label="Extracted Arguments")
479
+ out_result = gr.JSON(label="Tool Execution Output")
480
+
481
+ with gr.Row():
482
+ gr.Markdown("---")
483
+ gr.Markdown("### Latent Space Visualization")
484
+ plot_output = gr.Plot(label="Tool World Map")
485
+
486
+ def process_and_plot(user_prompt):
487
+ if not user_prompt or not user_prompt.strip():
488
+ # Return empty state and the default plot
489
+ return "", "", {}, {}, plot_tool_world()
490
+
491
+ prompt, tool_name, score, args_json, result_json = execute_tool(user_prompt)
492
+ fig = plot_tool_world(user_prompt)
493
+
494
+ # Safely load JSON strings into objects for the UI
495
+ try:
496
+ args_obj = json.loads(args_json)
497
+ except (json.JSONDecodeError, TypeError):
498
+ args_obj = {"error": "Invalid JSON in arguments", "raw": args_json}
499
+
500
+ try:
501
+ result_obj = json.loads(result_json)
502
+ except (json.JSONDecodeError, TypeError):
503
+ result_obj = {"error": "Invalid JSON in result", "raw": result_json}
504
+
505
+ return tool_name, score, args_obj, result_obj, fig
506
+
507
+ run_btn.click(
508
+ fn=process_and_plot,
509
+ inputs=inp,
510
+ outputs=[out_tool, out_score, out_args, out_result, plot_output]
511
+ )
512
+
513
+ # Load the initial plot when the app starts
514
+ demo.load(fn=lambda: plot_tool_world(None), inputs=None, outputs=plot_output)
515
+
516
+ demo.launch()