Skip to content

Commit f592e3d

Browse files
committed
fixed a problem with Stella's environment
Signed-off-by: Deborah Dahl <dahl@conversational-technologies.com>
1 parent 52e04f8 commit f592e3d

16 files changed

Lines changed: 546 additions & 190 deletions

assistantClient/assistantClient.py

Lines changed: 213 additions & 37 deletions
Large diffs are not rendered by default.

assistantClient/event_handlers.py

Lines changed: 54 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ def send_broadcast_to_agents(payload_obj, urls_to_send, status_callback=None, ui
318318
return all_responses
319319

320320

321-
def process_agent_responses(root, all_responses, floor_manager, update_conversation_history_callback, invited_agents=None, update_agent_textboxes_callback=None, extract_url_callback=None, manifest_cache=None, show_incoming_events: bool = False, directed_addressee=None, display_name_resolver=None):
321+
def process_agent_responses(root, all_responses, floor_manager, update_conversation_history_callback, invited_agents=None, update_agent_textboxes_callback=None, extract_url_callback=None, manifest_cache=None, show_incoming_events: bool = False, directed_addressee=None, display_name_resolver=None, sync_conversant_callback=None):
322322
"""Phase 2: Process all responses and update conversation history.
323323
324324
Args:
@@ -360,6 +360,17 @@ def _url_from_speaker_uri(speaker_uri):
360360
normalized = _normalize_agent_id(key)
361361
if normalized:
362362
manifest_cache[normalized] = assistantConversationalName
363+
364+
canonical_speaker_uri = manifest_speaker_uri or assistant_uri
365+
if sync_conversant_callback is not None:
366+
try:
367+
sync_conversant_callback(
368+
agent_url=manifest_service_url or target_url,
369+
speaker_uri=canonical_speaker_uri,
370+
conversational_name=assistantConversationalName,
371+
)
372+
except Exception:
373+
pass
363374

364375
# Update agent info with conversational name
365376
if invited_agents is not None and extract_url_callback is not None:
@@ -376,14 +387,14 @@ def _url_from_speaker_uri(speaker_uri):
376387
break
377388

378389
# Add agent to floor manager if active
379-
if floor_manager is not None and assistant_uri:
390+
if floor_manager is not None and canonical_speaker_uri:
380391
try:
381392
floor_manager.add_conversant(
382-
speaker_uri=assistant_uri,
393+
speaker_uri=canonical_speaker_uri,
383394
service_url=manifest_service_url,
384395
conversational_name=assistantConversationalName
385396
)
386-
print(f"Added {assistantConversationalName or assistant_uri} to floor manager")
397+
print(f"Added {assistantConversationalName or canonical_speaker_uri} to floor manager")
387398
except Exception as e:
388399
print(f"Failed to add agent to floor manager: {e}")
389400
else:
@@ -399,6 +410,15 @@ def _url_from_speaker_uri(speaker_uri):
399410
# Extract speaker info for conversation history
400411
speaker_uri = dialog_event.get("speakerUri", "Unknown")
401412
print(f"[DEBUG] Processing utterance - speakerUri from dialogEvent: {speaker_uri}")
413+
414+
if sync_conversant_callback is not None:
415+
try:
416+
sync_conversant_callback(
417+
agent_url=target_url,
418+
speaker_uri=speaker_uri if speaker_uri != "Unknown" else None,
419+
)
420+
except Exception:
421+
pass
402422

403423
# Get the conversational name for the actual speaker (from speakerUri in dialogEvent)
404424
speaker_conversational_name = None
@@ -484,7 +504,7 @@ def _url_from_speaker_uri(speaker_uri):
484504
)
485505

486506

487-
def forward_responses_to_agents(all_responses, urls_to_send, global_conversation, update_conversation_history_callback, status_callback=None, ui_pump_callback=None, directed_addressee=None, display_name_resolver=None):
507+
def forward_responses_to_agents(all_responses, urls_to_send, global_conversation, update_conversation_history_callback, status_callback=None, ui_pump_callback=None, directed_addressee=None, display_name_resolver=None, build_conversation_callback=None):
488508
"""Phase 3: Forward all responses to all other agents (after processing all initial responses).
489509
490510
Args:
@@ -502,7 +522,28 @@ def _url_from_speaker_uri(speaker_uri):
502522
return speaker_uri
503523
return None
504524

525+
def _current_conversation_state():
526+
if build_conversation_callback is not None:
527+
try:
528+
return build_conversation_callback()
529+
except Exception:
530+
pass
531+
return global_conversation
532+
533+
def _serialize_conversants(conversation_obj):
534+
return [
535+
{
536+
"identification": {
537+
"speakerUri": c.identification.speakerUri,
538+
"serviceUrl": c.identification.serviceUrl,
539+
"conversationalName": c.identification.conversationalName
540+
}
541+
}
542+
for c in getattr(conversation_obj, "conversants", []) or []
543+
]
544+
505545
for target_url, response_data, original_sender, incoming_events in all_responses:
546+
current_conversation = _current_conversation_state()
506547
print(f"\n=== FORWARDING CHECK ===")
507548
print(f"incoming_events count: {len(incoming_events)}")
508549
print(f"urls_to_send: {urls_to_send}")
@@ -533,17 +574,8 @@ def _url_from_speaker_uri(speaker_uri):
533574
forward_payload = {
534575
"openFloor": {
535576
"conversation": {
536-
"id": global_conversation.id,
537-
"conversants": [
538-
{
539-
"identification": {
540-
"speakerUri": c.identification.speakerUri,
541-
"serviceUrl": c.identification.serviceUrl,
542-
"conversationalName": c.identification.conversationalName
543-
}
544-
}
545-
for c in global_conversation.conversants
546-
]
577+
"id": current_conversation.id,
578+
"conversants": _serialize_conversants(current_conversation)
547579
},
548580
"sender": original_sender, # Preserve original sender, not client
549581
"events": broadcast_events # Forward events as broadcasts
@@ -559,7 +591,7 @@ def _url_from_speaker_uri(speaker_uri):
559591
except Exception:
560592
pass
561593
print(f"\n=== FORWARDING TO {other_agent_url} ===")
562-
print(f"Conversation ID: {global_conversation.id}")
594+
print(f"Conversation ID: {current_conversation.id}")
563595
print(f"Number of broadcast events: {len(broadcast_events)}")
564596
print(f"Broadcast events: {json.dumps(broadcast_events, indent=2)}")
565597
forward_response = _post_with_optional_ui_pump(
@@ -610,15 +642,15 @@ def _url_from_speaker_uri(speaker_uri):
610642
# Find speaker name by matching serviceUrl
611643
speaker_name = None
612644
speaker_service_url = _url_from_speaker_uri(speaker_uri)
613-
for c in global_conversation.conversants:
645+
for c in getattr(current_conversation, "conversants", []) or []:
614646
if c.identification.speakerUri == speaker_uri:
615647
speaker_name = c.identification.conversationalName
616648
speaker_service_url = c.identification.serviceUrl or speaker_service_url
617649
break
618650

619651
# If not found by speakerUri, try by serviceUrl
620652
if not speaker_name:
621-
for c in global_conversation.conversants:
653+
for c in getattr(current_conversation, "conversants", []) or []:
622654
if c.identification.serviceUrl == other_agent_url:
623655
speaker_name = c.identification.conversationalName
624656
speaker_service_url = c.identification.serviceUrl or speaker_service_url
@@ -673,20 +705,12 @@ def _url_from_speaker_uri(speaker_uri):
673705
response_broadcast_events.append(evt_copy)
674706

675707
# Create payload for recursive forwarding
708+
recursive_conversation = _current_conversation_state()
676709
recursive_payload = {
677710
"openFloor": {
678711
"conversation": {
679-
"id": global_conversation.id,
680-
"conversants": [
681-
{
682-
"identification": {
683-
"speakerUri": c.identification.speakerUri,
684-
"serviceUrl": c.identification.serviceUrl,
685-
"conversationalName": c.identification.conversationalName
686-
}
687-
}
688-
for c in global_conversation.conversants
689-
]
712+
"id": recursive_conversation.id,
713+
"conversants": _serialize_conversants(recursive_conversation)
690714
},
691715
"sender": responding_agent_sender,
692716
"events": response_broadcast_events

assistantClient/ui_components.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,24 @@ def create_ui_elements(root, known_agents):
308308
# Conversation history
309309
CTkLabel(content_frame, text="Conversation History:", font=HEADING_FONT).pack(pady=(5, 0))
310310
widgets['conversation_text'] = CTkTextbox(content_frame, wrap='word', height=294)
311+
312+
def _scroll_conversation_with_wheel(event):
313+
# Keep scrolling available even when the textbox is disabled/read-only.
314+
delta = getattr(event, "delta", 0)
315+
if delta:
316+
widgets['conversation_text'].yview_scroll(int(-delta / 120), "units")
317+
return "break"
318+
if getattr(event, "num", None) == 4:
319+
widgets['conversation_text'].yview_scroll(-1, "units")
320+
return "break"
321+
if getattr(event, "num", None) == 5:
322+
widgets['conversation_text'].yview_scroll(1, "units")
323+
return "break"
324+
return None
325+
326+
widgets['conversation_text'].bind("<MouseWheel>", _scroll_conversation_with_wheel)
327+
widgets['conversation_text'].bind("<Button-4>", _scroll_conversation_with_wheel)
328+
widgets['conversation_text'].bind("<Button-5>", _scroll_conversation_with_wheel)
311329
widgets['conversation_text'].configure(state='disabled')
312330
widgets['conversation_text'].pack(pady=5, padx=20, fill="both")
313331

financial/agent_config.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
"serviceUrl": "http://localhost:8083",
77
"organization": "OpenVoiceNetwork",
88
"role": "assistant",
9-
"synopsis": "A financial agent that provides real-time stock data powered by Finnhub"
9+
"synopsis": "A financial agent that provides real-time stock data powered by Finnhub",
10+
"openFloorRoles": ["information"]
1011
},
1112
"capabilities": {
1213
"keyphrases": [

financial/mcp_server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ async def handler(websocket):
116116
finn_data = query_finnhub(intent, stock, time_text=time_text)
117117
response = {"intent": intent, "stock": stock, "data": finn_data}
118118
else:
119-
response = {"error": "Could not parse intent or stock"}
119+
response = {"error": "Could not parse both intent and stock"}
120120

121121
except Exception as exc:
122122
logger.exception("Error handling MCP websocket message")

financial/template_agent.py

Lines changed: 72 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import json
2525
import logging
2626
import os
27+
import re
2728
from typing import Any, Callable, Dict, List
2829

2930
# Import OpenFloor components
@@ -228,8 +229,40 @@ def _is_only_agent_on_floor(self, in_envelope: Envelope) -> bool:
228229
if not isinstance(conversants, list):
229230
conversants = []
230231

231-
# Floor-awareness is based on incoming conversant count.
232-
return len(conversants) <= 1
232+
# Be conservative: missing/empty conversants means floor composition is unknown.
233+
if not conversants:
234+
return False
235+
236+
self_speaker_normalized = self._normalize_endpoint_id(self._manifest.identification.speakerUri)
237+
self_service_normalized = self._normalize_endpoint_id(self._manifest.identification.serviceUrl)
238+
known_conversant_ids = []
239+
240+
for conversant in conversants:
241+
identification = getattr(conversant, "identification", None)
242+
if identification is None and isinstance(conversant, dict):
243+
identification = conversant.get("identification", {})
244+
245+
if isinstance(identification, dict):
246+
speaker_uri = identification.get("speakerUri")
247+
service_url = identification.get("serviceUrl")
248+
else:
249+
speaker_uri = getattr(identification, "speakerUri", None)
250+
service_url = getattr(identification, "serviceUrl", None)
251+
252+
normalized_speaker = self._normalize_endpoint_id(speaker_uri)
253+
normalized_service = self._normalize_endpoint_id(service_url)
254+
if normalized_speaker:
255+
known_conversant_ids.append(normalized_speaker)
256+
if normalized_service:
257+
known_conversant_ids.append(normalized_service)
258+
259+
if not known_conversant_ids:
260+
return False
261+
262+
unique_ids = set(known_conversant_ids)
263+
self_ids = {value for value in (self_speaker_normalized, self_service_normalized) if value}
264+
265+
return len(unique_ids) == 1 and bool(self_ids & unique_ids)
233266

234267
def _is_directly_addressed(self, event: UtteranceEvent, user_text: str) -> bool:
235268
to_value = getattr(event, "to", None)
@@ -295,26 +328,17 @@ def _handle_utterance(self, event: UtteranceEvent, in_envelope: Envelope, out_en
295328
logger.debug("[UTTERANCE] No text found in utterance event")
296329
return
297330

298-
parsed = utterance_handler.parse_query(user_text)
299-
has_stock = bool(parsed.get("stock"))
300-
has_intent = bool(parsed.get("intent"))
331+
is_directly_addressed = self._is_directly_addressed(event, user_text)
301332

302-
if not has_stock and not has_intent:
303-
if is_only_agent:
304-
dialog = DialogEvent(
305-
speakerUri=self._manifest.identification.speakerUri,
306-
features={"text": TextFeature(values=["that's outside of my expertise"])},
307-
)
308-
out_envelope.events.append(UtteranceEvent(dialogEvent=dialog))
309-
else:
310-
logger.debug("[UTTERANCE] Ignoring non-financial utterance while multiple agents are on the floor")
311-
return
312-
313-
if not (is_only_agent or self._is_directly_addressed(event, user_text)):
333+
if not (is_only_agent or is_directly_addressed):
314334
logger.debug("[UTTERANCE] Ignoring utterance; Finn is neither directly addressed nor the only agent on the floor")
315335
return
316336

317337
logger.debug("[UTTERANCE] Received: %s", user_text)
338+
339+
parsed = utterance_handler.parse_query(user_text)
340+
parsed_stock = parsed.get("stock")
341+
parsed_intent = parsed.get("intent")
318342

319343
# Call utterance handler with just text + plain context - returns text response
320344
response_text = utterance_handler.process_utterance(
@@ -323,12 +347,27 @@ def _handle_utterance(self, event: UtteranceEvent, in_envelope: Envelope, out_en
323347
)
324348

325349
if not response_text:
350+
if parsed_stock and not parsed_intent:
351+
stock_name = utterance_handler.get_stock_display_name(parsed_stock)
352+
dialog = DialogEvent(
353+
speakerUri=self._manifest.identification.speakerUri,
354+
features={"text": TextFeature(values=[f"what information would you like about {stock_name}?"])}
355+
)
356+
out_envelope.events.append(UtteranceEvent(dialogEvent=dialog))
357+
logger.debug("[UTTERANCE] Clarifying stock-only query for %s", stock_name)
358+
return
359+
360+
if is_only_agent or is_directly_addressed:
361+
dialog = DialogEvent(
362+
speakerUri=self._manifest.identification.speakerUri,
363+
features={"text": TextFeature(values=["that's outside of my expertise"])}
364+
)
365+
out_envelope.events.append(UtteranceEvent(dialogEvent=dialog))
326366
logger.debug("[UTTERANCE] No response generated")
327367
return
328368

329369
responding_to_name = self._resolve_utterance_speaker_name(event, in_envelope)
330-
_known_agent_markers = ("lucky", "prudence", "finn")
331-
if responding_to_name and any(m in responding_to_name.lower() for m in _known_agent_markers):
370+
if responding_to_name:
332371
response_text = f"{responding_to_name}: {response_text}"
333372

334373
logger.debug("[UTTERANCE] Response: %s", response_text)
@@ -514,13 +553,18 @@ def _infer_agent_name_from_uri(uri: str) -> str:
514553
normalized = self._normalize_endpoint_id(uri)
515554
if not normalized:
516555
return ""
517-
if "finn" in normalized or ":8083" in normalized:
518-
return "Finn"
519-
if "prudence" in normalized or ":8084" in normalized:
520-
return "Prudence"
521-
if "lucky" in normalized or ":8085" in normalized:
522-
return "Lucky"
523-
return ""
556+
557+
candidate = str(uri or "").strip().rstrip("/")
558+
if "/" in candidate:
559+
candidate = candidate.split("/")[-1]
560+
if ":" in candidate:
561+
candidate = candidate.split(":")[-1]
562+
563+
candidate = re.sub(r"^agent:\s*", "", candidate, flags=re.IGNORECASE).strip()
564+
candidate = re.sub(r"[^A-Za-z0-9 _-]", " ", candidate).strip()
565+
if not candidate:
566+
return ""
567+
return " ".join(part.capitalize() for part in candidate.split())
524568

525569
conversation = getattr(in_envelope, 'conversation', None)
526570
conversants = getattr(conversation, 'conversants', []) if conversation else []
@@ -817,7 +861,8 @@ def load_manifest_from_config(config_path: str = "agent_config.json") -> Manifes
817861
conversationalName=ident_data.get('conversationalName', 'TemplateAgent'),
818862
organization=ident_data.get('organization', 'YourOrganization'),
819863
role=ident_data.get('role', 'assistant'),
820-
synopsis=ident_data.get('synopsis', 'A template OpenFloor agent')
864+
synopsis=ident_data.get('synopsis', 'A template OpenFloor agent'),
865+
openFloorRoles=ident_data.get('openFloorRoles', ['information'])
821866
)
822867

823868
# Build Capabilities

0 commit comments

Comments
 (0)