Skip to content

Commit 6c98991

Browse files
committed
feat: speaker resolution, peer reactions, market indices, and solo-agent fallback
- Fix speaker name resolution: normalize endpoint URIs to canonical agent names (Finn/Lucky/Prudence) in both template and utterance handlers to enable proper peer-to-peer interactions with name-prefixed responses Signed-off-by Deborah Dahl <dahl@conversational-technologies.com> - Fix peer reaction counter: reset _peer_reaction_count on each fresh user question so agents continue reacting to each other through conversational turns - Add out-of-domain fallback: agents alone on floor now respond with "that's outside of my expertise" for queries outside their domain expertise - Add market indices support via ETF proxies: users can now query index data (Dow, S&P 500, Nasdaq-100, Russell 2000) on Finnhub free tier by mapping ^DJI→DIA, ^GSPC→SPY, ^IXIC→QQQ, ^RUT→IWM with implied index levels in responses - Tune agent personalities: Prudence disapproves of reckless/speculative advice; Lucky disapproves of overly cautious, fear-driven investing - Improve web-floor UI: increase conversation history font size to clamp(1rem, 2.1vw, 1.15rem); make utterance textbox full panel width; allow resize handle up to 80vh max height Files modified: - lucky/utterance_handler.py (canonical names, peer reset, personality) - lucky/template_agent.py (speaker resolution, solo-agent detection, fallback) - prudence/utterance_handler.py (canonical names, peer reset, personality) - prudence/template_agent.py (speaker resolution, solo-agent detection, fallback) - financial/utterance_handler.py (indices, proxy ETFs, formatted responses) - financial/template_agent.py (speaker resolution, fallback text standardization) - floor-implementations/.../styles.css (font, width, height tuning)
1 parent 279b183 commit 6c98991

34 files changed

Lines changed: 4417 additions & 14 deletions

.vscode/launch.json

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,26 @@
8383
"cwd": "${workspaceFolder}/financial",
8484
"justMyCode": false,
8585
"python": "${workspaceFolder}/financial/.venv/Scripts/python.exe"
86+
},
87+
{
88+
"name": "Python: prudence Flask server",
89+
"type": "debugpy",
90+
"request": "launch",
91+
"program": "${workspaceFolder}/prudence/flask_server.py",
92+
"console": "integratedTerminal",
93+
"cwd": "${workspaceFolder}/prudence",
94+
"justMyCode": false,
95+
"python": "${workspaceFolder}/gemini-agent/.venv/Scripts/python.exe"
96+
},
97+
{
98+
"name": "Python: lucky Flask server",
99+
"type": "debugpy",
100+
"request": "launch",
101+
"program": "${workspaceFolder}/lucky/flask_server.py",
102+
"console": "integratedTerminal",
103+
"cwd": "${workspaceFolder}/lucky",
104+
"justMyCode": false,
105+
"python": "${workspaceFolder}/gemini-agent/.venv/Scripts/python.exe"
86106
}
87107
],
88108
"compounds": [
@@ -94,7 +114,9 @@
94114
"Python: erin Flask server",
95115
"Python: verity Flask server",
96116
"Python: time-agent Flask server",
97-
"Python: financial Flask server"
117+
"Python: financial Flask server",
118+
"Python: prudence Flask server",
119+
"Python: lucky Flask server"
98120
]
99121
},
100122
{

agent-template/envelope_handler.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,13 @@ def serialize_envelope(envelope: Envelope) -> str:
119119
JSON string representation of the envelope
120120
"""
121121
try:
122+
# Do not echo incoming conversants back to clients.
123+
if envelope and getattr(envelope, "conversation", None) is not None:
124+
try:
125+
setattr(envelope.conversation, "conversants", None)
126+
except Exception:
127+
pass
128+
122129
# Debug-only: inspect the envelope before serialization
123130
if logger.isEnabledFor(logging.DEBUG):
124131
logger.debug("[SERIALIZE] Envelope has %d events", len(envelope.events))

agent-template/template_agent.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -249,24 +249,31 @@ def _handle_utterance(self, event: UtteranceEvent, in_envelope: Envelope, out_en
249249
try:
250250
# Extract user text from the OpenFloor event
251251
user_text = self._extract_text_from_utterance_event(event)
252+
incoming_speaker_uri = (self._extract_speaker_uri_from_utterance_event(event) or "").strip().lower()
253+
self_speaker_uri = str(self._manifest.identification.speakerUri or "").strip().lower()
254+
responding_to_name = self._resolve_utterance_speaker_name(event, in_envelope)
255+
256+
if incoming_speaker_uri and self_speaker_uri and incoming_speaker_uri == self_speaker_uri:
257+
logger.debug("[UTTERANCE] Ignoring self-originated utterance")
258+
return
252259

253260
if not user_text:
254261
logger.debug("[UTTERANCE] No text found in utterance event")
255262
return
256263

257264
logger.debug("[UTTERANCE] Received: %s", user_text)
258265

259-
# Call utterance handler with just text + plain context - returns text response
266+
# Call utterance handler with text, agent name, and speaker name for multi-agent reactions
260267
response_text = utterance_handler.process_utterance(
261268
user_text,
262269
agent_name=self._manifest.identification.conversationalName,
270+
speaker_name=responding_to_name,
263271
)
264272

265273
if not response_text:
266274
logger.debug("[UTTERANCE] No response generated")
267275
return
268276

269-
responding_to_name = self._resolve_utterance_speaker_name(event, in_envelope)
270277
if responding_to_name:
271278
response_text = f"{responding_to_name}: {response_text}"
272279

@@ -532,7 +539,7 @@ def _handle_uninvite(self, event: UninviteEvent, in_envelope: Envelope, out_enve
532539

533540
dialog = DialogEvent(
534541
speakerUri=self._manifest.identification.speakerUri,
535-
features=[TextFeature.from_text(farewell)]
542+
features={"text": TextFeature(values=[farewell])}
536543
)
537544

538545
utterance_event = UtteranceEvent(dialogEvent=dialog)

erin/envelope_handler.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,13 @@ def serialize_envelope(envelope: Envelope) -> str:
119119
JSON string representation of the envelope
120120
"""
121121
try:
122+
# Do not echo incoming conversants back to clients.
123+
if envelope and getattr(envelope, "conversation", None) is not None:
124+
try:
125+
setattr(envelope.conversation, "conversants", None)
126+
except Exception:
127+
pass
128+
122129
# Debug-only: inspect the envelope before serialization
123130
if logger.isEnabledFor(logging.DEBUG):
124131
logger.debug("[SERIALIZE] Envelope has %d events", len(envelope.events))

erin/template_agent.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,11 @@ def _handle_utterance(self, event: UtteranceEvent, in_envelope: Envelope, out_en
298298
try:
299299
# Extract user text from the OpenFloor event
300300
user_text = self._extract_text_from_utterance_event(event)
301+
incoming_speaker_uri = (self._extract_speaker_uri_from_utterance_event(event) or "").strip().lower()
302+
self_speaker_uri = str(self._manifest.identification.speakerUri or "").strip().lower()
303+
if incoming_speaker_uri and self_speaker_uri and incoming_speaker_uri == self_speaker_uri:
304+
logger.debug("[UTTERANCE] Ignoring self-originated utterance")
305+
return
301306

302307
if not user_text:
303308
logger.debug("[UTTERANCE] No text found in utterance event")

financial/envelope_handler.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,13 @@ def serialize_envelope(envelope: Envelope) -> str:
119119
JSON string representation of the envelope
120120
"""
121121
try:
122+
# Do not echo incoming conversants back to clients.
123+
if envelope and getattr(envelope, "conversation", None) is not None:
124+
try:
125+
setattr(envelope.conversation, "conversants", None)
126+
except Exception:
127+
pass
128+
122129
# Debug-only: inspect the envelope before serialization
123130
if logger.isEnabledFor(logging.DEBUG):
124131
logger.debug("[SERIALIZE] Envelope has %d events", len(envelope.events))

financial/mcp_server.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ def query_finnhub(intent, stock, time_text=None):
4848
url = f"{base_url}/company-news?symbol={stock}&from={datetime.utcfromtimestamp(from_ts).strftime('%Y-%m-%d')}&to={datetime.utcfromtimestamp(to_ts).strftime('%Y-%m-%d')}&token={FINNHUB_API_KEY}"
4949
elif intent == "EARNINGS_QUERY":
5050
url = f"{base_url}/stock/earnings?symbol={stock}&token={FINNHUB_API_KEY}"
51+
elif intent == "DIVIDEND_QUERY":
52+
url = f"{base_url}/stock/dividend?symbol={stock}&token={FINNHUB_API_KEY}"
5153
elif intent == "HISTORY_QUERY":
5254
url = f"{base_url}/stock/candle?symbol={stock}&resolution=D&from={from_ts}&to={to_ts}&token={FINNHUB_API_KEY}"
5355

financial/template_agent.py

Lines changed: 94 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,40 @@ def _register_handlers(self):
221221
context_handler = getattr(self, "on_context", None)
222222
if context_handler is not None:
223223
context_handler += self._handle_context
224+
225+
def _is_only_agent_on_floor(self, in_envelope: Envelope) -> bool:
226+
conversation = getattr(in_envelope, "conversation", None)
227+
conversants = getattr(conversation, "conversants", []) if conversation else []
228+
if not isinstance(conversants, list):
229+
conversants = []
230+
231+
# Floor-awareness is based on incoming conversant count.
232+
return len(conversants) <= 1
233+
234+
def _is_directly_addressed(self, event: UtteranceEvent, user_text: str) -> bool:
235+
to_value = getattr(event, "to", None)
236+
if to_value is None and isinstance(event, dict):
237+
to_value = event.get("to")
238+
239+
# Explicit OpenFloor addressee targeting this agent.
240+
if to_value is not None:
241+
return self._is_addressed_to_me(event)
242+
243+
text = (user_text or "").strip().lower()
244+
if not text:
245+
return False
246+
247+
agent_name = str(getattr(self._manifest.identification, "conversationalName", "")).strip().lower()
248+
if not agent_name:
249+
return False
250+
251+
direct_prefixes = (
252+
f"{agent_name}:",
253+
f"{agent_name},",
254+
f"{agent_name} ",
255+
f"@{agent_name}",
256+
)
257+
return text.startswith(direct_prefixes)
224258

225259
# =========================================================================
226260
# UTTERANCE EVENT - Delegated to utterance_handler.py
@@ -249,11 +283,37 @@ def _handle_utterance(self, event: UtteranceEvent, in_envelope: Envelope, out_en
249283
try:
250284
# Extract user text from the OpenFloor event
251285
user_text = self._extract_text_from_utterance_event(event)
286+
incoming_speaker_uri = (self._extract_speaker_uri_from_utterance_event(event) or "").strip().lower()
287+
self_speaker_uri = str(self._manifest.identification.speakerUri or "").strip().lower()
288+
is_only_agent = self._is_only_agent_on_floor(in_envelope)
289+
290+
if incoming_speaker_uri and self_speaker_uri and incoming_speaker_uri == self_speaker_uri:
291+
logger.debug("[UTTERANCE] Ignoring self-originated utterance")
292+
return
252293

253294
if not user_text:
254295
logger.debug("[UTTERANCE] No text found in utterance event")
255296
return
256297

298+
parsed = utterance_handler.parse_query(user_text)
299+
has_stock = bool(parsed.get("stock"))
300+
has_intent = bool(parsed.get("intent"))
301+
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)):
314+
logger.debug("[UTTERANCE] Ignoring utterance; Finn is neither directly addressed nor the only agent on the floor")
315+
return
316+
257317
logger.debug("[UTTERANCE] Received: %s", user_text)
258318

259319
# Call utterance handler with just text + plain context - returns text response
@@ -267,7 +327,8 @@ def _handle_utterance(self, event: UtteranceEvent, in_envelope: Envelope, out_en
267327
return
268328

269329
responding_to_name = self._resolve_utterance_speaker_name(event, in_envelope)
270-
if responding_to_name:
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):
271332
response_text = f"{responding_to_name}: {response_text}"
272333

273334
logger.debug("[UTTERANCE] Response: %s", response_text)
@@ -445,7 +506,20 @@ def _resolve_utterance_speaker_name(self, event: UtteranceEvent, in_envelope: En
445506
speaker_uri = self._extract_speaker_uri_from_utterance_event(event)
446507
if not speaker_uri:
447508
return ""
448-
if "assistantclientconvener" in str(speaker_uri).strip().lower():
509+
speaker_normalized = self._normalize_endpoint_id(speaker_uri)
510+
if "assistantclientconvener" in speaker_normalized:
511+
return ""
512+
513+
def _infer_agent_name_from_uri(uri: str) -> str:
514+
normalized = self._normalize_endpoint_id(uri)
515+
if not normalized:
516+
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"
449523
return ""
450524

451525
conversation = getattr(in_envelope, 'conversation', None)
@@ -461,15 +535,28 @@ def _resolve_utterance_speaker_name(self, event: UtteranceEvent, in_envelope: En
461535

462536
if isinstance(identification, dict):
463537
conversant_speaker = identification.get('speakerUri')
538+
conversant_service = identification.get('serviceUrl')
464539
conversational_name = identification.get('conversationalName')
465540
else:
466541
conversant_speaker = getattr(identification, 'speakerUri', None)
542+
conversant_service = getattr(identification, 'serviceUrl', None)
467543
conversational_name = getattr(identification, 'conversationalName', None)
468544

469-
if conversant_speaker and str(conversant_speaker).strip().lower() == str(speaker_uri).strip().lower():
470-
return conversational_name or speaker_uri
471-
472-
return speaker_uri
545+
conversant_speaker_normalized = self._normalize_endpoint_id(conversant_speaker)
546+
conversant_service_normalized = self._normalize_endpoint_id(conversant_service)
547+
if speaker_normalized and (
548+
speaker_normalized == conversant_speaker_normalized
549+
or speaker_normalized == conversant_service_normalized
550+
):
551+
return (
552+
conversational_name
553+
or _infer_agent_name_from_uri(conversant_speaker)
554+
or _infer_agent_name_from_uri(conversant_service)
555+
or _infer_agent_name_from_uri(speaker_uri)
556+
or ""
557+
)
558+
559+
return _infer_agent_name_from_uri(speaker_uri) or ""
473560

474561
# =========================================================================
475562
# CONVERSATION LIFECYCLE EVENTS
@@ -503,7 +590,7 @@ def _handle_invite(self, event: InviteEvent, in_envelope: Envelope, out_envelope
503590
if self.joinedFloor:
504591
greeting = f"Hi, I'm {agent_name}. I've joined the floor and I'm ready to help!"
505592
else:
506-
greeting = f"Hi, I'm {agent_name}. How can I help you today?"
593+
greeting = f"Hi, I'm {agent_name}. I can give you real-time stock information."
507594

508595
# Create greeting utterance
509596
dialog = DialogEvent(

0 commit comments

Comments
 (0)