dtka commited on
Commit
e5caa62
·
1 Parent(s): 8dbbb07

Adding Agent

Browse files
Files changed (6) hide show
  1. .gitignore +2 -0
  2. README.md +31 -3
  3. agents_registry.json +114 -0
  4. app.py +359 -0
  5. requirements.txt +4 -0
  6. utils/discovery.py +32 -0
.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # Environment variables
2
+ .env
README.md CHANGED
@@ -1,14 +1,42 @@
1
  ---
2
  title: Collective Intelligence Orchestrator
3
- emoji: 🚀
4
- colorFrom: indigo
5
  colorTo: indigo
6
  sdk: gradio
7
  sdk_version: 5.33.1
8
  app_file: app.py
9
  pinned: false
10
  license: apache-2.0
11
- short_description: Activate a living swarm of AI agents.
12
  ---
13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
  title: Collective Intelligence Orchestrator
3
+ emoji: 🐢
4
+ colorFrom: purple
5
  colorTo: indigo
6
  sdk: gradio
7
  sdk_version: 5.33.1
8
  app_file: app.py
9
  pinned: false
10
  license: apache-2.0
11
+ short_description: Gradio-based interface coordinates a network of agents
12
  ---
13
 
14
+ # Collective Intelligence Orchestrator
15
+
16
+ This Gradio-based interface coordinates a network of autonomous AI agents using Hugging Face's MCP protocol.
17
+
18
+ ## Agents in the Network:
19
+ - Climate Sensor
20
+ - Policy Modeler
21
+ - Economic Forecast
22
+ - Media Monitor
23
+ - Public Health
24
+ - NGO Matcher
25
+
26
+ Submit a real-world scenario, and watch the swarm collaborate in real time.
27
+
28
+ ## Test Case Example:
29
+ Input:
30
+
31
+ - “Massive flooding in coastal Bangladesh, affecting over 200,000 residents. Power outages, displacement, and rising waterborne diseases reported.”
32
+
33
+ Expected output:
34
+
35
+ - Climate Sensor: Critical anomaly detected.
36
+ - Policy Modeler: Emergency zoning, sanitation relief, medical deployment.
37
+ - Economic Forecast: Estimated GDP loss, recovery timeline.
38
+ - Media Monitor: High sentiment panic, moderate misinfo risk.
39
+ - Public Health: Cholera risk, hospital overload.
40
+ - NGO Matcher: Suggest 2–3 orgs that can help.
41
+
42
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
agents_registry.json ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "version": "1.0.0",
3
+ "last_updated": "2025-06-10T08:47:58Z",
4
+ "agents": [
5
+ {
6
+ "id": "ngo-matcher-001",
7
+ "name": "NGO Matcher",
8
+ "description": "Matches humanitarian needs with relevant NGOs and aid organizations",
9
+ "endpoint": "https://dtka-collective-intelligence-ngo-matching-agent.hf.space/agent",
10
+ "file_url": "https://huggingface.co/spaces/dtka/collective-intelligence-ngo-matching-agent/resolve/main/",
11
+ "icon": "🤝",
12
+ "api_spec": "/openapi.json",
13
+ "type": "matching",
14
+ "categories": ["humanitarian", "coordination"],
15
+ "tags": ["ngo", "aid", "matching"],
16
+ "status": "active",
17
+ "version": "1.0.0",
18
+ "capabilities": ["ngo_matching", "resource_allocation"],
19
+ "rate_limit": 100,
20
+ "timeout_seconds": 30,
21
+ "auth_required": false
22
+ },
23
+ {
24
+ "id": "media-monitor-001",
25
+ "name": "Media Monitor",
26
+ "description": "Tracks and analyzes media coverage and social media for crisis events",
27
+ "endpoint": "https://dtka-collective-intelligence-media-monitoring-agent.hf.space/agent",
28
+ "file_url": "https://huggingface.co/spaces/dtka/collective-intelligence-media-monitoring-agent/resolve/main/",
29
+ "icon": "📰",
30
+ "api_spec": "/openapi.json",
31
+ "type": "monitoring",
32
+ "categories": ["media", "sentiment_analysis"],
33
+ "tags": ["news", "social_media", "sentiment"],
34
+ "status": "active",
35
+ "version": "1.0.0",
36
+ "capabilities": ["news_aggregation", "sentiment_analysis", "trend_detection"],
37
+ "rate_limit": 200,
38
+ "timeout_seconds": 20,
39
+ "auth_required": false
40
+ },
41
+ {
42
+ "id": "policy-modeler-001",
43
+ "name": "Policy Modeler",
44
+ "description": "Models policy impacts and suggests regulatory responses to crises",
45
+ "endpoint": "https://dtka-collective-intelligence-policy-modeler-agent.hf.space/agent",
46
+ "file_url": "https://huggingface.co/spaces/dtka/collective-intelligence-policy-modeler-agent/resolve/main/",
47
+ "icon": "📜",
48
+ "api_spec": "/openapi.json",
49
+ "type": "modeling",
50
+ "categories": ["policy", "regulation"],
51
+ "tags": ["policy_analysis", "regulations", "impact_modeling"],
52
+ "status": "active",
53
+ "version": "1.0.0",
54
+ "capabilities": ["policy_simulation", "impact_analysis"],
55
+ "rate_limit": 50,
56
+ "timeout_seconds": 45,
57
+ "auth_required": true
58
+ },
59
+ {
60
+ "id": "climate-sensor-001",
61
+ "name": "Climate Sensor",
62
+ "description": "Monitors and analyzes environmental and climate-related data",
63
+ "endpoint": "https://dtka-collective-intelligence-climate-sensor.hf.space/agent",
64
+ "file_url": "https://huggingface.co/spaces/dtka/collective-intelligence-climate-sensor/resolve/main/",
65
+ "icon": "🌦️",
66
+ "api_spec": "/openapi.json",
67
+ "type": "monitoring",
68
+ "categories": ["environment", "climate"],
69
+ "tags": ["sensor_data", "environmental_monitoring", "climate_data"],
70
+ "status": "active",
71
+ "version": "1.0.0",
72
+ "capabilities": ["sensor_data_analysis", "anomaly_detection"],
73
+ "rate_limit": 150,
74
+ "timeout_seconds": 25,
75
+ "auth_required": false
76
+ },
77
+ {
78
+ "id": "public-health-001",
79
+ "name": "Public Health",
80
+ "description": "Tracks and analyzes public health data and disease outbreaks",
81
+ "endpoint": "https://dtka-collective-intelligence-public-health-agent.hf.space/agent",
82
+ "file_url": "https://huggingface.co/spaces/dtka/collective-intelligence-public-health-agent/resolve/main/",
83
+ "icon": "🏥",
84
+ "api_spec": "/openapi.json",
85
+ "type": "analysis",
86
+ "categories": ["health", "epidemiology"],
87
+ "tags": ["disease_tracking", "healthcare", "outbreak_analysis"],
88
+ "status": "active",
89
+ "version": "1.0.0",
90
+ "capabilities": ["disease_surveillance", "risk_assessment"],
91
+ "rate_limit": 75,
92
+ "timeout_seconds": 35,
93
+ "auth_required": true
94
+ },
95
+ {
96
+ "id": "economic-forecast-001",
97
+ "name": "Economic Forecast",
98
+ "description": "Provides economic impact analysis and forecasting for crisis scenarios",
99
+ "endpoint": "https://dtka-collective-intelligence-economic-forecast-agent.hf.space/agent",
100
+ "file_url": "https://huggingface.co/spaces/dtka/collective-intelligence-economic-forecast-agent/resolve/main/",
101
+ "icon": "💹",
102
+ "api_spec": "/openapi.json",
103
+ "type": "forecasting",
104
+ "categories": ["economics", "finance"],
105
+ "tags": ["economic_analysis", "forecasting", "impact_assessment"],
106
+ "status": "active",
107
+ "version": "1.0.0",
108
+ "capabilities": ["economic_modeling", "trend_analysis"],
109
+ "rate_limit": 60,
110
+ "timeout_seconds": 40,
111
+ "auth_required": true
112
+ }
113
+ ]
114
+ }
app.py ADDED
@@ -0,0 +1,359 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import requests
3
+ import gradio as gr
4
+ import anthropic
5
+ import yaml
6
+ import hashlib
7
+ import json
8
+
9
+ # Anthropic API Setup
10
+ anthropic_client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
11
+
12
+ # ---- Agent Discovery Logic ----
13
+
14
+ AGENT_ICONS = {
15
+ "Climate Sensor": "🌦️",
16
+ "Policy Modeler": "📜",
17
+ "Economic Forecast": "💹",
18
+ "Media Monitor": "📰",
19
+ "Public Health": "🏥",
20
+ "NGO Matcher": "🤝"
21
+ }
22
+
23
+ def fetch_registry():
24
+ # Load from local file first, fall back to remote if not found
25
+ local_registry = "agents_registry.json"
26
+ if os.path.exists(local_registry):
27
+ print("Loading agents from local registry file")
28
+ try:
29
+ with open(local_registry, 'r', encoding='utf-8') as f:
30
+ return json.load(f)
31
+ except Exception as e:
32
+ print(f"Error loading local registry: {e}")
33
+
34
+ # Fall back to remote registry
35
+ remote_url = "https://huggingface.co/spaces/dtka/collective-intelligence-orchestrator/resolve/main/agents_registry.json"
36
+ print(f"Fetching agents from remote registry: {remote_url}")
37
+ try:
38
+ res = requests.get(remote_url, timeout=5)
39
+ res.raise_for_status()
40
+ return res.json()
41
+ except Exception as e:
42
+ print(f"Error fetching remote registry: {e}")
43
+ return None
44
+
45
+ def fetch_agent_yaml(space_url):
46
+ try:
47
+ res = requests.get(f"{space_url}/agent.yaml")
48
+ return yaml.safe_load(res.text)
49
+ except:
50
+ return None
51
+
52
+ def compute_agent_embedding(text):
53
+ # Handle None or empty input
54
+ if not text:
55
+ return 0
56
+ # Simulated embedding using a hash — replace with real embedding logic if desired
57
+ return int(hashlib.md5(text.encode()).hexdigest(), 16) % 10000
58
+
59
+ def discover_agents_from_registry():
60
+ registry = fetch_registry()
61
+ if not registry or 'agents' not in registry:
62
+ print("No valid registry data found")
63
+ return [], [], []
64
+
65
+ tools = []
66
+ cards = []
67
+ index = []
68
+
69
+ for agent in registry['agents']:
70
+ if not agent.get('status') == 'active':
71
+ continue
72
+
73
+ try:
74
+ # Use metadata from registry or fetch from agent if needed
75
+ name = agent.get('name', 'Unnamed Agent')
76
+ description = agent.get('description', 'No description available')
77
+ endpoint = agent.get('endpoint')
78
+ icon = agent.get('icon', '')
79
+
80
+ # Create tool function using the agent's endpoint
81
+ def tool_func(input_text, agent_endpoint=endpoint, agent_name=name):
82
+ try:
83
+ resp = requests.post(
84
+ agent_endpoint,
85
+ json={"input": input_text},
86
+ timeout=agent.get('timeout_seconds', 30)
87
+ )
88
+ resp.raise_for_status()
89
+ return resp.json().get("output", "No structured output.")
90
+ except Exception as e:
91
+ print(f"Error calling {agent_name} agent: {str(e)}")
92
+ return f"Error: Failed to call {agent_name} agent - {str(e)}"
93
+
94
+ # Compute embedding for semantic search
95
+ emb = compute_agent_embedding(f"{name} {description} {' '.join(agent.get('tags', []))}")
96
+
97
+ # Add to tools with all necessary metadata
98
+ tools.append((
99
+ name,
100
+ icon,
101
+ description,
102
+ tool_func,
103
+ emb,
104
+ agent.get('categories', []),
105
+ agent.get('capabilities', [])
106
+ ))
107
+
108
+ # Update index for semantic search
109
+ index.append((emb, name))
110
+
111
+ # Add card for UI
112
+ cards.append((icon, name, description, agent.get('categories', [])))
113
+
114
+ except Exception as e:
115
+ print(f"Error processing agent {agent.get('name', 'unknown')}: {str(e)}")
116
+ continue
117
+
118
+ print(f"Successfully loaded {len(tools)} agents from registry")
119
+ return tools, cards, index
120
+
121
+ # ---- Claude Orchestrator ----
122
+
123
+ def match_agents_by_vector(input_text, tools, index):
124
+ if not tools or not index:
125
+ return []
126
+
127
+ input_emb = compute_agent_embedding(input_text)
128
+
129
+ # Calculate similarity scores for all agents
130
+ scored_agents = []
131
+ for i, (emb, name) in enumerate(index):
132
+ # Simple similarity based on vector distance
133
+ similarity = 1 / (1 + abs(emb - input_emb))
134
+ scored_agents.append((similarity, tools[i]))
135
+
136
+ # Sort by similarity score (descending)
137
+ scored_agents.sort(reverse=True, key=lambda x: x[0])
138
+
139
+ # Return top matching agents (above threshold or top 3)
140
+ threshold = 0.3 # Adjust based on your needs
141
+ return [agent for score, agent in scored_agents if score > threshold][:5] # Limit to top 5 matches
142
+
143
+ def claude_conductor(message, history, tools=None, index=None):
144
+ if tools is None:
145
+ tools = []
146
+ if index is None:
147
+ index = []
148
+
149
+ selected_tools = match_agents_by_vector(message, tools, index)
150
+
151
+ tools_description = "\n".join(
152
+ f"- {icon} {name}: {desc} (Categories: {', '.join(categories) if categories else 'None'})"
153
+ for name, icon, desc, _, _, categories, _ in selected_tools
154
+ ) if selected_tools else "No relevant tools matched."
155
+
156
+ # Format the conversation history for Claude
157
+ conversation = []
158
+ for user_msg, bot_msg in history:
159
+ if user_msg:
160
+ conversation.append({"role": "user", "content": user_msg})
161
+ if bot_msg:
162
+ conversation.append({"role": "assistant", "content": bot_msg})
163
+
164
+ # Add the current user message
165
+ conversation.append({"role": "user", "content": message})
166
+
167
+ # Create system prompt
168
+ system_prompt = """You are a coordinator of autonomous AI agents solving real-world crises.
169
+ Analyze the user's input and determine which tools to use to gather information.
170
+ Here are the available agents for this task:
171
+ {tools_description}
172
+ Provide a clear, concise response based on the available information."""
173
+
174
+ try:
175
+ # Call Claude API
176
+ response = anthropic_client.messages.create(
177
+ model="claude-3-sonnet-20240229",
178
+ max_tokens=1000,
179
+ system=system_prompt,
180
+ messages=conversation,
181
+ temperature=0.7
182
+ )
183
+
184
+ # Return the response text
185
+ if response.content and len(response.content) > 0:
186
+ return response.content[0].text
187
+ return "I couldn't generate a response. Please try again."
188
+
189
+ except Exception as e:
190
+ print(f"Error calling Claude API: {str(e)}")
191
+ return f"An error occurred while processing your request: {str(e)}"
192
+
193
+ # ---- Launch Gradio ChatInterface ----
194
+
195
+ if __name__ == "__main__":
196
+ print("Starting application...")
197
+ tools, cards, index = discover_agents_from_registry()
198
+ print(f"Discovered {len(tools)} tools, {len(cards)} cards, {len(index)} index entries")
199
+ if not tools:
200
+ print("WARNING: No tools discovered. The UI may not display correctly.")
201
+
202
+ with gr.Blocks(
203
+ theme=gr.themes.Soft(primary_hue="blue", secondary_hue="cyan"),
204
+ title="Collective Intelligence Orchestrator"
205
+ ) as demo:
206
+ gr.Markdown("""
207
+ # 🧠 Collective Intelligence Orchestrator
208
+ _Activate a living swarm of AI agents._
209
+
210
+ Enter a real-world scenario (e.g., natural disaster, policy failure, humanitarian crisis), and let the orchestrator dynamically coordinate a swarm response using multiple autonomous MCP agents.
211
+
212
+ **Author**: [@dtka](https://huggingface.co/dtka)
213
+ **Project Docs**: [GitHub Repo](https://github.com/dtka/collective-intelligence-networks)
214
+ **Hackathon**: [Hugging Face MCP Hackathon](https://huggingface.co/Agents-MCP-Hackathon)
215
+ """)
216
+
217
+ with gr.Row():
218
+ with gr.Column(scale=1):
219
+ gr.Markdown("### 🧩 Available Agents")
220
+ if not cards:
221
+ gr.Markdown("⚠️ No agents discovered. Please check agents_registry.json or try again later.")
222
+ for icon, name, desc, categories in cards:
223
+ categories_html = f"<br><span style='font-size: 0.8em; color: #666;'><i>Categories: {', '.join(categories) if categories else 'General'}</i></span>" if categories else ""
224
+ gr.Markdown(
225
+ f"<b>{icon} {name}</b><br>"
226
+ f"<span style='font-size: 0.9em;'>{desc}</span>"
227
+ f"{categories_html}",
228
+ render=True,
229
+ elem_id="agent-card"
230
+ )
231
+
232
+ with gr.Column(scale=2):
233
+ # Create the chat interface with explicit buttons
234
+ with gr.Row():
235
+ with gr.Column(scale=8):
236
+ # Chatbot display
237
+ chatbot = gr.Chatbot(
238
+ height=500,
239
+ show_copy_button=True,
240
+ show_label=False,
241
+ container=True,
242
+ bubble_full_width=True,
243
+ placeholder="Start a conversation...",
244
+ elem_id="chatbot"
245
+ )
246
+
247
+ # Input area with buttons
248
+ with gr.Row():
249
+ msg = gr.Textbox(
250
+ placeholder="Describe a crisis or scenario...",
251
+ container=False,
252
+ scale=8,
253
+ min_width=200,
254
+ show_label=False
255
+ )
256
+ submit_btn = gr.Button("Send to Swarm", variant="primary", scale=1)
257
+ stop_btn = gr.Button("Stop", variant="stop", scale=1, visible=False)
258
+
259
+ # Additional buttons
260
+ with gr.Row():
261
+ clear_btn = gr.Button("Clear Chat")
262
+ retry_btn = gr.Button("Retry")
263
+
264
+ # Format messages for the chat interface
265
+ def format_messages(history):
266
+ formatted = []
267
+ for user_msg, bot_msg in history:
268
+ if user_msg:
269
+ formatted.append((user_msg, None))
270
+ if bot_msg is not None:
271
+ if formatted and formatted[-1][1] is None:
272
+ formatted[-1] = (formatted[-1][0], bot_msg)
273
+ else:
274
+ formatted.append((None, bot_msg))
275
+ return formatted
276
+
277
+ # Set up button click handlers
278
+ def user(user_message, history):
279
+ if not user_message.strip():
280
+ return "", history
281
+ return "", history + [[user_message, None]]
282
+
283
+ def bot(history):
284
+ if not history or not history[-1][0]:
285
+ return history
286
+
287
+ # Get the current message and previous conversation
288
+ current_message = history[-1][0]
289
+ prev_messages = history[:-1]
290
+
291
+ # Format history for Claude
292
+ formatted_history = []
293
+ for user_msg, bot_msg in prev_messages:
294
+ if user_msg:
295
+ formatted_history.append((user_msg, bot_msg or ""))
296
+
297
+ # Get response from Claude
298
+ try:
299
+ bot_message = claude_conductor(current_message, formatted_history, tools, index)
300
+ history[-1][1] = bot_message
301
+ except Exception as e:
302
+ print(f"Error in bot response: {e}")
303
+ history[-1][1] = "Sorry, I encountered an error. Please try again."
304
+
305
+ return history
306
+
307
+ # Message submission handler
308
+ def process_message(user_message, history):
309
+ if not user_message.strip():
310
+ return "", history
311
+ history = history + [[user_message, None]]
312
+ return "", history
313
+
314
+ # Connect UI elements
315
+ submit_event = msg.submit(
316
+ process_message,
317
+ [msg, chatbot],
318
+ [msg, chatbot],
319
+ queue=False
320
+ ).then(
321
+ bot,
322
+ chatbot,
323
+ chatbot
324
+ )
325
+
326
+ submit_btn.click(
327
+ process_message,
328
+ [msg, chatbot],
329
+ [msg, chatbot],
330
+ queue=False
331
+ ).then(
332
+ bot,
333
+ chatbot,
334
+ chatbot
335
+ )
336
+
337
+ # Clear chat button
338
+ clear_btn.click(lambda: [], None, chatbot, queue=False)
339
+
340
+ # Retry button
341
+ def retry_last(history):
342
+ if not history:
343
+ return history
344
+ if history[-1][1] is not None:
345
+ history[-1][1] = None
346
+ return history
347
+
348
+ retry_btn.click(
349
+ retry_last,
350
+ chatbot,
351
+ chatbot,
352
+ queue=False
353
+ ).then(
354
+ bot,
355
+ chatbot,
356
+ chatbot
357
+ )
358
+
359
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ gradio==5.33.1
2
+ requests==2.32.3
3
+ pyyaml==6.0.2
4
+ anthropic==0.53.0
utils/discovery.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ import gradio as gr
3
+ import yaml
4
+
5
+ def fetch_agent_yaml(space_url):
6
+ try:
7
+ res = requests.get(f"{space_url}/agent.yaml")
8
+ return yaml.safe_load(res.text)
9
+ except:
10
+ return None
11
+
12
+ def build_agent_tool(agent_info, endpoint_url):
13
+ def tool_func(input_text):
14
+ try:
15
+ resp = requests.post(f"{endpoint_url}/agent", json={"input": input_text}, timeout=10)
16
+ return resp.json().get("output", "No structured output.")
17
+ except Exception as e:
18
+ return f"Error: {str(e)}"
19
+
20
+ return gr.TextTool(
21
+ name=agent_info.get("name", "Unnamed Agent"),
22
+ description=agent_info.get("description", "No description."),
23
+ func=tool_func
24
+ )
25
+
26
+ def discover_agents_from_urls(agent_urls):
27
+ tools = []
28
+ for url in agent_urls:
29
+ metadata = fetch_agent_yaml(url)
30
+ if metadata:
31
+ tools.append(build_agent_tool(metadata, url))
32
+ return tools