fix(P0): Add function invoking marker and improve ChatMessage extraction
Browse filesChanges:
1. Add __function_invoking_chat_client__ marker to HuggingFaceChatClient
- Without this marker, agent_framework ignores tools passed to the client
- The warning "does not support function invoking" is now eliminated
2. Improve _extract_text in AdvancedOrchestrator:
- Handle ChatMessage.contents (list of FunctionCallContent, TextContent)
- Extract tool call names when text content is empty
- Return empty string instead of object repr for graceful fallback
3. Fix WorkflowOutputEvent handling:
- Use _extract_text instead of str() to avoid object repr in output
- Provide meaningful fallback message when no text available
These fixes ensure the HuggingFace Free Tier can properly:
- Declare function calling capability to the framework
- Display tool calls and text content in events
- Show clean output instead of <ChatMessage object at ...>
- src/clients/huggingface.py +4 -0
- src/orchestrators/advanced.py +32 -18
|
@@ -29,6 +29,10 @@ logger = structlog.get_logger()
|
|
| 29 |
class HuggingFaceChatClient(BaseChatClient): # type: ignore[misc]
|
| 30 |
"""Adapter for HuggingFace Inference API with full function calling support."""
|
| 31 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
def __init__(
|
| 33 |
self,
|
| 34 |
model_id: str | None = None,
|
|
|
|
| 29 |
class HuggingFaceChatClient(BaseChatClient): # type: ignore[misc]
|
| 30 |
"""Adapter for HuggingFace Inference API with full function calling support."""
|
| 31 |
|
| 32 |
+
# Marker to tell agent_framework that this client supports function calling
|
| 33 |
+
# Without this, the framework warns and ignores tools
|
| 34 |
+
__function_invoking_chat_client__ = True
|
| 35 |
+
|
| 36 |
def __init__(
|
| 37 |
self,
|
| 38 |
model_id: str | None = None,
|
|
@@ -337,32 +337,44 @@ The final output should be a structured research report."""
|
|
| 337 |
"""
|
| 338 |
Defensively extract text from a message object.
|
| 339 |
|
| 340 |
-
|
|
|
|
| 341 |
"""
|
| 342 |
if not message:
|
| 343 |
return ""
|
| 344 |
|
| 345 |
-
# Priority 1: .
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
if hasattr(message, "content") and message.content:
|
| 347 |
content = message.content
|
| 348 |
-
|
|
|
|
| 349 |
if isinstance(content, list):
|
| 350 |
return " ".join([str(c.text) for c in content if hasattr(c, "text")])
|
| 351 |
-
return str(content)
|
| 352 |
-
|
| 353 |
-
# Priority 2: .text (standard, but sometimes buggy/missing)
|
| 354 |
-
if hasattr(message, "text") and message.text:
|
| 355 |
-
# Verify it's not the object itself or a repr string
|
| 356 |
-
text = str(message.text)
|
| 357 |
-
if text.startswith("<") and "object at" in text:
|
| 358 |
-
# Likely a repr string, ignore if possible
|
| 359 |
-
pass
|
| 360 |
-
else:
|
| 361 |
-
return text
|
| 362 |
|
| 363 |
-
# Fallback:
|
| 364 |
-
#
|
| 365 |
-
return
|
| 366 |
|
| 367 |
def _get_event_type_for_agent(self, agent_name: str) -> str:
|
| 368 |
"""Map agent name to appropriate event type.
|
|
@@ -456,9 +468,11 @@ The final output should be a structured research report."""
|
|
| 456 |
|
| 457 |
elif isinstance(event, WorkflowOutputEvent):
|
| 458 |
if event.data:
|
|
|
|
|
|
|
| 459 |
return AgentEvent(
|
| 460 |
type="complete",
|
| 461 |
-
message=
|
| 462 |
iteration=iteration,
|
| 463 |
)
|
| 464 |
|
|
|
|
| 337 |
"""
|
| 338 |
Defensively extract text from a message object.
|
| 339 |
|
| 340 |
+
Handles ChatMessage objects from both OpenAI and HuggingFace clients.
|
| 341 |
+
ChatMessage has: .text (str), .contents (list of content objects)
|
| 342 |
"""
|
| 343 |
if not message:
|
| 344 |
return ""
|
| 345 |
|
| 346 |
+
# Priority 1: .text (standard ChatMessage text content)
|
| 347 |
+
if hasattr(message, "text") and message.text:
|
| 348 |
+
text = message.text
|
| 349 |
+
# Verify it's actually a string, not the object itself
|
| 350 |
+
if isinstance(text, str) and not (text.startswith("<") and "object at" in text):
|
| 351 |
+
return text
|
| 352 |
+
|
| 353 |
+
# Priority 2: .contents (list of FunctionCallContent, TextContent, etc.)
|
| 354 |
+
# This handles tool call responses from HuggingFace
|
| 355 |
+
if hasattr(message, "contents") and message.contents:
|
| 356 |
+
parts = []
|
| 357 |
+
for content in message.contents:
|
| 358 |
+
# TextContent has .text
|
| 359 |
+
if hasattr(content, "text") and content.text:
|
| 360 |
+
parts.append(str(content.text))
|
| 361 |
+
# FunctionCallContent has .name and .arguments
|
| 362 |
+
elif hasattr(content, "name"):
|
| 363 |
+
parts.append(f"[Tool: {content.name}]")
|
| 364 |
+
if parts:
|
| 365 |
+
return " ".join(parts)
|
| 366 |
+
|
| 367 |
+
# Priority 3: .content (legacy - some frameworks use singular)
|
| 368 |
if hasattr(message, "content") and message.content:
|
| 369 |
content = message.content
|
| 370 |
+
if isinstance(content, str):
|
| 371 |
+
return content
|
| 372 |
if isinstance(content, list):
|
| 373 |
return " ".join([str(c.text) for c in content if hasattr(c, "text")])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
|
| 375 |
+
# Fallback: Return empty string instead of repr
|
| 376 |
+
# The repr is useless for display purposes
|
| 377 |
+
return ""
|
| 378 |
|
| 379 |
def _get_event_type_for_agent(self, agent_name: str) -> str:
|
| 380 |
"""Map agent name to appropriate event type.
|
|
|
|
| 468 |
|
| 469 |
elif isinstance(event, WorkflowOutputEvent):
|
| 470 |
if event.data:
|
| 471 |
+
# Use _extract_text to properly handle ChatMessage objects
|
| 472 |
+
text = self._extract_text(event.data)
|
| 473 |
return AgentEvent(
|
| 474 |
type="complete",
|
| 475 |
+
message=text if text else "Research complete (no synthesis)",
|
| 476 |
iteration=iteration,
|
| 477 |
)
|
| 478 |
|