Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions agex/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
FailEvent,
OutputEvent,
SuccessEvent,
SummaryEvent,
TaskStartEvent,
)
from .llm import LLMClient, connect_llm
Expand Down Expand Up @@ -42,6 +43,7 @@
"FailEvent",
"ClarifyEvent",
"ErrorEvent",
"SummaryEvent",
# Agent Registry
"clear_agent_registry",
# LLM Client Factory
Expand Down
3 changes: 0 additions & 3 deletions agex/agent/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ def __init__(
primer: str | None = None,
timeout_seconds: float = 5.0,
max_iterations: int = 10,
max_tokens: int = 2**16,
# Agent identification
name: str | None = None,
# Optional curated capabilities primer
Expand All @@ -78,7 +77,6 @@ def __init__(
primer: A string to guide the agent's behavior.
timeout_seconds: The maximum time in seconds for a single action evaluation.
max_iterations: The maximum number of think-act cycles for a task.
max_tokens: The maximum number of tokens to use for context rendering.
name: Unique identifier for this agent (for sub-agent namespacing).
capabilities_primer: Optional curated capabilities primer.
llm_client: An instantiated LLMClient for the agent to use.
Expand All @@ -91,7 +89,6 @@ def __init__(
primer=primer,
timeout_seconds=timeout_seconds,
max_iterations=max_iterations,
max_tokens=max_tokens,
name=name,
capabilities_primer=capabilities_primer,
llm_client=llm_client,
Expand Down
2 changes: 0 additions & 2 deletions agex/agent/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ def __init__(
primer: str | None,
timeout_seconds: float,
max_iterations: int,
max_tokens: int,
# Agent identification
name: str | None = None,
# Optional curated capabilities primer (overrides rendered registrations when set)
Expand All @@ -90,7 +89,6 @@ def __init__(
self.capabilities_primer = capabilities_primer
self.timeout_seconds = timeout_seconds
self.max_iterations = max_iterations
self.max_tokens = max_tokens

# Create LLM client using the resolved configuration
self.llm_client = llm_client or connect_llm()
Expand Down
45 changes: 42 additions & 3 deletions agex/agent/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,33 @@ def _render_object_as_html(obj: Any) -> str:
return f"<pre style='background: #fff; padding: 8px; border-radius: 3px; margin: 0; color: #24292e; font-family: monospace;'>{escaped_content}</pre>"
# Check if object has _repr_html_ method (pandas DataFrames, plotly Figures, etc.)
elif hasattr(obj, "_repr_html_"):
# For DataFrames, ensure we use pandas default display settings for HTML
# (not the temporarily-modified settings used for LLM token optimization)
from agex.render.primitives import is_dataframe

if is_dataframe(obj):
try:
import pandas as pd

# Save current settings
old_max_rows = pd.options.display.max_rows
old_min_rows = pd.options.display.min_rows

# Restore pandas defaults for HTML rendering
pd.options.display.max_rows = 60 # pandas default
pd.options.display.min_rows = 10 # pandas default

try:
html_content = _suppress_stdout(lambda: obj._repr_html_())
return f"<div style='margin: 5px 0;'>{html_content}</div>"
finally:
# Restore whatever settings were active
pd.options.display.max_rows = old_max_rows
pd.options.display.min_rows = old_min_rows
except ImportError:
pass

# Non-DataFrame or pandas not available: use regular rendering
# Suppress stdout during _repr_html_ call to prevent Plotly figures
# (and other objects with IPython display hooks) from printing HTML to stdout
# in NiceGUI/IPython environments
Expand Down Expand Up @@ -745,8 +772,6 @@ def _repr_markdown_(self) -> str:

def _repr_html_(self) -> str:
"""Rich HTML representation for IPython/Jupyter environments."""
import html

compression_ratio = (
f"{self.original_tokens} → {self.full_detail_tokens} tokens"
if self.full_detail_tokens > 0
Expand All @@ -755,7 +780,21 @@ def _repr_html_(self) -> str:
header = (
f"📊 Summary of {self.summarized_event_count} events ({compression_ratio})"
)
content = _event_section(header, html.escape(self.summary), "#6a737d")

# Convert markdown summary to HTML for rich display
try:
import markdown

summary_html = markdown.markdown(
self.summary, extensions=["fenced_code", "nl2br", "sane_lists"]
)
except ImportError:
# Fallback to escaped text if markdown not available
import html

summary_html = f"<pre>{html.escape(self.summary)}</pre>"

content = _event_section(header, summary_html, "#6a737d")

return _event_html_container(
"📝",
Expand Down
2 changes: 1 addition & 1 deletion agex/agent/loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ def setup_on_event(event):
# Main task loop
for _ in range(self.max_iterations):
# Check if event log needs summarization before querying LLM
maybe_summarize_event_log(self, exec_state)
maybe_summarize_event_log(self, exec_state, on_event)

# Get all events from state for LLM
from agex.state.log import get_events_from_log
Expand Down
56 changes: 43 additions & 13 deletions agex/agent/summarization.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,47 @@ class SummarizationError(Exception):
pass


SUMMARIZATION_SYSTEM_MESSAGE = """You are summarizing AI agent execution history.
SUMMARIZATION_SYSTEM_MESSAGE = """You are summarizing what happened in a completed AI agent interaction.

The agent operates in a Python REPL environment where it:
- Thinks through problems step-by-step
- Writes and executes Python code
- Observes results and adjusts its approach
- Signals completion with task control functions (task_success, task_continue, task_fail, task_clarify)
You will see a transcript showing:
- What the user requested
- The agent's thinking and code execution
- Results and outputs
- Task completion

Your task is to provide a concise summary that captures:
- Key actions the agent took and why
- Important decisions and reasoning
- Outcomes and results
- Any errors or issues encountered
YOUR ROLE: You are an EXTERNAL OBSERVER writing a summary. You are NOT the agent. Do not respond as if you are continuing the agent's work.

Focus on what the agent accomplished and learned. Be concise but preserve essential context for future actions."""
YOUR JOB: Write a detailed summary describing what the agent accomplished. Use THIRD PERSON ("The agent did X" or "The system did X").

REQUIRED FORMAT - Write prose like this:
"The user requested X. The agent retrieved Y data from Z API/source. It found W results with N items/records. [Any issues encountered and how they were resolved]."

def maybe_summarize_event_log(agent: "BaseAgent", state: State) -> None:
EXAMPLES OF GOOD SUMMARIES:
- "The user requested upcoming kids' calendar events. The agent queried the Google Calendar API and retrieved 61 events spanning 3 months. It filtered for school and sports activities, identified 3 scheduling conflicts, and presented them in a DataFrame showing dates, event types, and locations."
- "The user asked to analyze sales data. The agent loaded a CSV with 1,247 transactions, calculated monthly totals, and identified the top 3 performing products (generating $45K in revenue). It created a visualization showing the sales trend over time."
- "The user wanted to send an email. The agent encountered an authentication error with the primary API, switched to a different endpoint, and successfully sent the message to 5 recipients with delivery confirmation."

EXAMPLES OF BAD SUMMARIES (DO NOT DO THIS):
❌ "Task completed" - too vague, no information
❌ "✅ Task completed" - just an emoji, tells nothing
❌ "I will now..." - WRONG! You are not the agent, don't continue the conversation
❌ "Let me check the calendar..." - WRONG! Don't act as the agent
❌ "OutputEvent with DataFrame" - technical jargon, not narrative

BE SPECIFIC about:
- What the user wanted
- What data/APIs/resources the agent accessed
- What concrete results were produced (counts, values, outcomes)
- Any problems the agent solved

Write in PAST TENSE, THIRD PERSON ("The agent did...", "It found..."), ACTIVE VOICE.

If a pre-existing summary is provided, include it in your summary as well.

Remember: You are summarizing a COMPLETED interaction, not participating in it."""


def maybe_summarize_event_log(agent: "BaseAgent", state: State, on_event=None) -> None:
"""
Check if event log needs summarization and perform it if necessary.

Expand All @@ -50,6 +73,7 @@ def maybe_summarize_event_log(agent: "BaseAgent", state: State) -> None:
Args:
agent: Agent with llm_client and watermark configuration
state: State containing the event log
on_event: Optional callback to notify about the SummaryEvent

Raises:
SummarizationError: If LLM summarization call fails
Expand Down Expand Up @@ -116,10 +140,12 @@ def maybe_summarize_event_log(agent: "BaseAgent", state: State) -> None:
original_tokens = sum(e.full_detail_tokens for e in events_to_summarize)

# Call LLM to generate summary (pass events directly for multimodal support)
# Use same max_tokens as normal completions for detailed summaries
try:
summary_text = agent.llm_client.summarize(
system=SUMMARIZATION_SYSTEM_MESSAGE,
content=events_to_summarize,
max_tokens=16384, # Same as normal completions (16K tokens)
)
except Exception as e:
raise SummarizationError(
Expand All @@ -137,3 +163,7 @@ def maybe_summarize_event_log(agent: "BaseAgent", state: State) -> None:

# Replace old events with summary
replace_oldest_events_with_summary(state, num_to_summarize, summary)

# Notify event handler if provided
if on_event is not None:
on_event(summary)
9 changes: 6 additions & 3 deletions agex/agent/task_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,12 @@ def _smart_render_for_task_input(value: Any) -> str:
rich content like DataFrames, arrays, and other complex objects
in their natural representation when possible.
"""
from agex.render.primitives import HI_DETAIL_BUDGET

renderer = ValueRenderer(
max_len=4096, # Generous limit for rich task display
max_depth=4, # Deep enough for complex nested objects
max_items=50, # Show more items than default for task context
max_len=HI_DETAIL_BUDGET * 4, # Align with token budget (~32K chars)
max_depth=4,
max_items=50,
token_budget=HI_DETAIL_BUDGET, # Enable iterative DataFrame rendering
)
return renderer.render(value, compact=False)
38 changes: 36 additions & 2 deletions agex/llm/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,14 +150,48 @@ def _prepare_summarization_content(
Returns:
(is_multimodal, processed_content)
- If text: (False, text_string)
- If events: (True, rendered_messages)
- If events: (True, conversation_transcript_as_string)
"""
if isinstance(content, list):
# Import here to avoid circular dependency
from agex.render.events import render_events_as_markdown

messages = render_events_as_markdown(content)
return (True, messages)

# Format as a transcript for summarization
# Instead of sending alternating user/assistant messages (confusing),
# send the entire conversation as a single text block to summarize
transcript_parts = []
for msg in messages:
role = msg.get("role", "unknown").upper()
content_value = msg.get("content", "")

# Handle both string and list content
if isinstance(content_value, list):
# Extract text from content parts
text_parts = []
for part in content_value:
if isinstance(part, dict) and part.get("type") == "text":
text_parts.append(part.get("text", ""))
content_value = "\n".join(text_parts)

transcript_parts.append(f"[{role}]:\n{content_value}\n")

transcript = "\n".join(transcript_parts)
framed_content = f"""You are an external observer summarizing a completed interaction.
DO NOT respond as if you are the agent in this conversation.
DO NOT continue the conversation or take actions.

Below is the HISTORICAL TRANSCRIPT to summarize:

---BEGIN TRANSCRIPT---
{transcript}
---END TRANSCRIPT---

Write your summary of what happened in this interaction."""

# Return as text (False) since we've converted it to a transcript
return (False, framed_content)
else:
return (False, content)

Expand Down
Loading