Spaces:
Sleeping
Sleeping
Commit ·
64462d2
1
Parent(s): e33886d
added session and memory
Browse files- agent_framework/__init__.py +29 -1
- agent_framework/agent.py +166 -13
- agent_framework/callbacks.py +42 -0
- agent_framework/llm.py +44 -36
- agent_framework/memory.py +283 -0
- agent_framework/models.py +89 -0
- agent_framework/tools.py +33 -6
- agent_framework/utils.py +35 -19
- agent_tools/example_usage.py +2 -2
- agent_tools/file_tools.py +12 -5
- agent_tools/web_tools.py +39 -0
- example_agent.py +41 -0
- pyproject.toml +3 -0
- rag/embeddings.py +1 -5
- test_session.py +108 -0
- web_app/README.md +63 -0
- web_app/app.py +244 -0
- web_app/static/index.html +1012 -0
- web_app/uploads/610Report.pdf +0 -0
agent_framework/__init__.py
CHANGED
|
@@ -7,9 +7,22 @@ from .models import (
|
|
| 7 |
ContentItem,
|
| 8 |
Event,
|
| 9 |
ExecutionContext,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
)
|
| 11 |
from .tools import BaseTool, FunctionTool, tool
|
| 12 |
-
from .llm import LlmClient, LlmRequest, LlmResponse
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
from .agent import Agent, AgentResult
|
| 14 |
from .mcp import load_mcp_tools
|
| 15 |
from .utils import (
|
|
@@ -18,6 +31,7 @@ from .utils import (
|
|
| 18 |
function_to_tool_definition,
|
| 19 |
mcp_tools_to_openai_format,
|
| 20 |
display_trace,
|
|
|
|
| 21 |
)
|
| 22 |
|
| 23 |
__all__ = [
|
|
@@ -28,6 +42,11 @@ __all__ = [
|
|
| 28 |
"ContentItem",
|
| 29 |
"Event",
|
| 30 |
"ExecutionContext",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
# Tools
|
| 32 |
"BaseTool",
|
| 33 |
"FunctionTool",
|
|
@@ -36,17 +55,26 @@ __all__ = [
|
|
| 36 |
"LlmClient",
|
| 37 |
"LlmRequest",
|
| 38 |
"LlmResponse",
|
|
|
|
| 39 |
# Agent
|
| 40 |
"Agent",
|
| 41 |
"AgentResult",
|
| 42 |
# MCP
|
| 43 |
"load_mcp_tools",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
# Utils
|
| 45 |
"function_to_input_schema",
|
| 46 |
"format_tool_definition",
|
| 47 |
"function_to_tool_definition",
|
| 48 |
"mcp_tools_to_openai_format",
|
| 49 |
"display_trace",
|
|
|
|
| 50 |
]
|
| 51 |
|
| 52 |
__version__ = "0.1.0"
|
|
|
|
| 7 |
ContentItem,
|
| 8 |
Event,
|
| 9 |
ExecutionContext,
|
| 10 |
+
Session,
|
| 11 |
+
ToolConfirmation,
|
| 12 |
+
PendingToolCall,
|
| 13 |
+
BaseSessionManager,
|
| 14 |
+
InMemorySessionManager,
|
| 15 |
)
|
| 16 |
from .tools import BaseTool, FunctionTool, tool
|
| 17 |
+
from .llm import LlmClient, LlmRequest, LlmResponse, build_messages
|
| 18 |
+
from .memory import (
|
| 19 |
+
count_tokens,
|
| 20 |
+
apply_sliding_window,
|
| 21 |
+
apply_compaction,
|
| 22 |
+
apply_summarization,
|
| 23 |
+
ContextOptimizer,
|
| 24 |
+
)
|
| 25 |
+
from .callbacks import create_optimizer_callback
|
| 26 |
from .agent import Agent, AgentResult
|
| 27 |
from .mcp import load_mcp_tools
|
| 28 |
from .utils import (
|
|
|
|
| 31 |
function_to_tool_definition,
|
| 32 |
mcp_tools_to_openai_format,
|
| 33 |
display_trace,
|
| 34 |
+
format_trace,
|
| 35 |
)
|
| 36 |
|
| 37 |
__all__ = [
|
|
|
|
| 42 |
"ContentItem",
|
| 43 |
"Event",
|
| 44 |
"ExecutionContext",
|
| 45 |
+
"Session",
|
| 46 |
+
"ToolConfirmation",
|
| 47 |
+
"PendingToolCall",
|
| 48 |
+
"BaseSessionManager",
|
| 49 |
+
"InMemorySessionManager",
|
| 50 |
# Tools
|
| 51 |
"BaseTool",
|
| 52 |
"FunctionTool",
|
|
|
|
| 55 |
"LlmClient",
|
| 56 |
"LlmRequest",
|
| 57 |
"LlmResponse",
|
| 58 |
+
"build_messages",
|
| 59 |
# Agent
|
| 60 |
"Agent",
|
| 61 |
"AgentResult",
|
| 62 |
# MCP
|
| 63 |
"load_mcp_tools",
|
| 64 |
+
# Memory
|
| 65 |
+
"count_tokens",
|
| 66 |
+
"apply_sliding_window",
|
| 67 |
+
"apply_compaction",
|
| 68 |
+
"apply_summarization",
|
| 69 |
+
"ContextOptimizer",
|
| 70 |
+
"create_optimizer_callback",
|
| 71 |
# Utils
|
| 72 |
"function_to_input_schema",
|
| 73 |
"format_tool_definition",
|
| 74 |
"function_to_tool_definition",
|
| 75 |
"mcp_tools_to_openai_format",
|
| 76 |
"display_trace",
|
| 77 |
+
"format_trace",
|
| 78 |
]
|
| 79 |
|
| 80 |
__version__ = "0.1.0"
|
agent_framework/agent.py
CHANGED
|
@@ -1,20 +1,22 @@
|
|
| 1 |
"""Agent class for executing multi-step reasoning with tools."""
|
| 2 |
|
| 3 |
from dataclasses import dataclass
|
| 4 |
-
from typing import List, Optional, Type, Callable
|
| 5 |
-
from
|
| 6 |
-
from pydantic import BaseModel
|
| 7 |
from .tools import tool
|
| 8 |
import inspect
|
| 9 |
import json
|
| 10 |
|
| 11 |
-
from pydantic_core.core_schema import str_schema
|
| 12 |
from .models import (
|
| 13 |
ExecutionContext,
|
| 14 |
Event,
|
| 15 |
Message,
|
| 16 |
ToolCall,
|
| 17 |
-
ToolResult
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
)
|
| 19 |
from .tools import BaseTool
|
| 20 |
from .llm import LlmClient, LlmRequest, LlmResponse
|
|
@@ -25,6 +27,8 @@ class AgentResult:
|
|
| 25 |
"""Result of an agent execution."""
|
| 26 |
output: str | BaseModel
|
| 27 |
context: ExecutionContext
|
|
|
|
|
|
|
| 28 |
|
| 29 |
|
| 30 |
class Agent:
|
|
@@ -39,7 +43,9 @@ class Agent:
|
|
| 39 |
name: str = "agent",
|
| 40 |
output_type: Optional[Type[BaseModel]] = None,
|
| 41 |
before_tool_callbacks: List[Callable] = None,
|
| 42 |
-
after_tool_callbacks: List[Callable] = None
|
|
|
|
|
|
|
| 43 |
|
| 44 |
):
|
| 45 |
self.model = model
|
|
@@ -53,6 +59,10 @@ class Agent:
|
|
| 53 |
self.before_tool_callbacks = before_tool_callbacks or []
|
| 54 |
self.after_tool_callbacks = after_tool_callbacks or []
|
| 55 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
def _setup_tools(self, tools: List[BaseTool]) -> List[BaseTool]:
|
| 57 |
if self.output_type is not None:
|
| 58 |
@tool(
|
|
@@ -71,9 +81,38 @@ class Agent:
|
|
| 71 |
async def run(
|
| 72 |
self,
|
| 73 |
user_input: str,
|
| 74 |
-
context: ExecutionContext = None
|
| 75 |
-
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
# Create or reuse context
|
| 78 |
if context is None:
|
| 79 |
context = ExecutionContext()
|
|
@@ -89,12 +128,34 @@ class Agent:
|
|
| 89 |
# Execute steps until completion or max steps reached
|
| 90 |
while not context.final_result and context.current_step < self.max_steps:
|
| 91 |
await self.step(context)
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
# Check if the last event is a final response
|
| 94 |
last_event = context.events[-1]
|
| 95 |
if self._is_final_response(last_event):
|
| 96 |
context.final_result = self._extract_final_result(last_event)
|
| 97 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
return AgentResult(output=context.final_result, context=context)
|
| 99 |
|
| 100 |
|
|
@@ -128,6 +189,23 @@ class Agent:
|
|
| 128 |
|
| 129 |
async def step(self, context: ExecutionContext):
|
| 130 |
"""Execute one step of the agent loop."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
|
| 132 |
llm_request = self._prepare_llm_request(context)
|
| 133 |
|
|
@@ -194,7 +272,8 @@ class Agent:
|
|
| 194 |
) -> List[ToolResult]:
|
| 195 |
tools_dict = {tool.name: tool for tool in self.tools}
|
| 196 |
results = []
|
| 197 |
-
|
|
|
|
| 198 |
for tool_call in tool_calls:
|
| 199 |
if tool_call.name not in tools_dict:
|
| 200 |
raise ValueError(f"Tool '{tool_call.name}' not found")
|
|
@@ -212,7 +291,17 @@ class Agent:
|
|
| 212 |
if result is not None:
|
| 213 |
tool_response = result
|
| 214 |
break
|
| 215 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
# Stage 2: Execute actual tool only if callback didn't provide a result
|
| 217 |
if tool_response is None:
|
| 218 |
try:
|
|
@@ -238,9 +327,73 @@ class Agent:
|
|
| 238 |
break
|
| 239 |
|
| 240 |
results.append(tool_result)
|
|
|
|
|
|
|
| 241 |
|
| 242 |
return results
|
| 243 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
# List of dangerous tools requiring approval
|
| 245 |
DANGEROUS_TOOLS = ["delete_file", "send_email", "execute_sql"]
|
| 246 |
|
|
|
|
| 1 |
"""Agent class for executing multi-step reasoning with tools."""
|
| 2 |
|
| 3 |
from dataclasses import dataclass
|
| 4 |
+
from typing import List, Optional, Type, Callable, Literal
|
| 5 |
+
from pydantic import BaseModel, Field
|
|
|
|
| 6 |
from .tools import tool
|
| 7 |
import inspect
|
| 8 |
import json
|
| 9 |
|
|
|
|
| 10 |
from .models import (
|
| 11 |
ExecutionContext,
|
| 12 |
Event,
|
| 13 |
Message,
|
| 14 |
ToolCall,
|
| 15 |
+
ToolResult,
|
| 16 |
+
PendingToolCall,
|
| 17 |
+
ToolConfirmation,
|
| 18 |
+
BaseSessionManager,
|
| 19 |
+
InMemorySessionManager
|
| 20 |
)
|
| 21 |
from .tools import BaseTool
|
| 22 |
from .llm import LlmClient, LlmRequest, LlmResponse
|
|
|
|
| 27 |
"""Result of an agent execution."""
|
| 28 |
output: str | BaseModel
|
| 29 |
context: ExecutionContext
|
| 30 |
+
status: Literal["complete", "pending", "error"] = "complete"
|
| 31 |
+
pending_tool_calls: list[PendingToolCall] = Field(default_factory=list)
|
| 32 |
|
| 33 |
|
| 34 |
class Agent:
|
|
|
|
| 43 |
name: str = "agent",
|
| 44 |
output_type: Optional[Type[BaseModel]] = None,
|
| 45 |
before_tool_callbacks: List[Callable] = None,
|
| 46 |
+
after_tool_callbacks: List[Callable] = None,
|
| 47 |
+
session_manager: BaseSessionManager | None = None
|
| 48 |
+
|
| 49 |
|
| 50 |
):
|
| 51 |
self.model = model
|
|
|
|
| 59 |
self.before_tool_callbacks = before_tool_callbacks or []
|
| 60 |
self.after_tool_callbacks = after_tool_callbacks or []
|
| 61 |
|
| 62 |
+
# Session manager
|
| 63 |
+
self.session_manager = session_manager or InMemorySessionManager()
|
| 64 |
+
|
| 65 |
+
|
| 66 |
def _setup_tools(self, tools: List[BaseTool]) -> List[BaseTool]:
|
| 67 |
if self.output_type is not None:
|
| 68 |
@tool(
|
|
|
|
| 81 |
async def run(
|
| 82 |
self,
|
| 83 |
user_input: str,
|
| 84 |
+
context: ExecutionContext = None,
|
| 85 |
+
session_id: Optional[str] = None,
|
| 86 |
+
tool_confirmations: Optional[List[ToolConfirmation]] = None
|
| 87 |
+
) -> AgentResult:
|
| 88 |
+
"""Execute the agent with optional session support.
|
| 89 |
+
|
| 90 |
+
Args:
|
| 91 |
+
user_input: User's input message
|
| 92 |
+
context: Optional execution context (creates new if None)
|
| 93 |
+
session_id: Optional session ID for persistent conversations
|
| 94 |
+
tool_confirmations: Optional list of tool confirmations for pending calls
|
| 95 |
+
"""
|
| 96 |
+
# Load or create session if session_id is provided
|
| 97 |
+
session = None
|
| 98 |
+
if session_id and self.session_manager:
|
| 99 |
+
session = await self.session_manager.get_or_create(session_id)
|
| 100 |
+
# Load session data into context if context is new
|
| 101 |
+
if context is None:
|
| 102 |
+
context = ExecutionContext()
|
| 103 |
+
# Restore events and state from session
|
| 104 |
+
context.events = session.events.copy()
|
| 105 |
+
context.state = session.state.copy()
|
| 106 |
+
context.execution_id = session.session_id
|
| 107 |
+
context.session_id = session_id
|
| 108 |
+
|
| 109 |
+
if tool_confirmations:
|
| 110 |
+
if context is None:
|
| 111 |
+
context = ExecutionContext()
|
| 112 |
+
context.state["tool_confirmations"] = [
|
| 113 |
+
c.model_dump() for c in tool_confirmations
|
| 114 |
+
]
|
| 115 |
+
|
| 116 |
# Create or reuse context
|
| 117 |
if context is None:
|
| 118 |
context = ExecutionContext()
|
|
|
|
| 128 |
# Execute steps until completion or max steps reached
|
| 129 |
while not context.final_result and context.current_step < self.max_steps:
|
| 130 |
await self.step(context)
|
| 131 |
+
# Check for pending confirmations after each step
|
| 132 |
+
if context.state.get("pending_tool_calls"):
|
| 133 |
+
pending_calls = [
|
| 134 |
+
PendingToolCall.model_validate(p)
|
| 135 |
+
for p in context.state["pending_tool_calls"]
|
| 136 |
+
]
|
| 137 |
+
# Save session state before returning
|
| 138 |
+
if session:
|
| 139 |
+
session.events = context.events
|
| 140 |
+
session.state = context.state
|
| 141 |
+
await self.session_manager.save(session)
|
| 142 |
+
return AgentResult(
|
| 143 |
+
status="pending",
|
| 144 |
+
context=context,
|
| 145 |
+
pending_tool_calls=pending_calls,
|
| 146 |
+
)
|
| 147 |
# Check if the last event is a final response
|
| 148 |
last_event = context.events[-1]
|
| 149 |
if self._is_final_response(last_event):
|
| 150 |
context.final_result = self._extract_final_result(last_event)
|
| 151 |
|
| 152 |
+
# Save session after execution completes
|
| 153 |
+
if session:
|
| 154 |
+
session.events = context.events
|
| 155 |
+
session.state = context.state
|
| 156 |
+
await self.session_manager.save(session)
|
| 157 |
+
|
| 158 |
+
|
| 159 |
return AgentResult(output=context.final_result, context=context)
|
| 160 |
|
| 161 |
|
|
|
|
| 189 |
|
| 190 |
async def step(self, context: ExecutionContext):
|
| 191 |
"""Execute one step of the agent loop."""
|
| 192 |
+
|
| 193 |
+
# Process pending confirmations if both are present (before preparing request)
|
| 194 |
+
if ("pending_tool_calls" in context.state and "tool_confirmations" in context.state):
|
| 195 |
+
confirmation_results = await self._process_confirmations(context)
|
| 196 |
+
|
| 197 |
+
# Add results as an event so they appear in contents
|
| 198 |
+
if confirmation_results:
|
| 199 |
+
confirmation_event = Event(
|
| 200 |
+
execution_id=context.execution_id,
|
| 201 |
+
author=self.name,
|
| 202 |
+
content=confirmation_results,
|
| 203 |
+
)
|
| 204 |
+
context.add_event(confirmation_event)
|
| 205 |
+
|
| 206 |
+
# Clear processed state
|
| 207 |
+
del context.state["pending_tool_calls"]
|
| 208 |
+
del context.state["tool_confirmations"]
|
| 209 |
|
| 210 |
llm_request = self._prepare_llm_request(context)
|
| 211 |
|
|
|
|
| 272 |
) -> List[ToolResult]:
|
| 273 |
tools_dict = {tool.name: tool for tool in self.tools}
|
| 274 |
results = []
|
| 275 |
+
pending_calls = [] # ADD THIS
|
| 276 |
+
|
| 277 |
for tool_call in tool_calls:
|
| 278 |
if tool_call.name not in tools_dict:
|
| 279 |
raise ValueError(f"Tool '{tool_call.name}' not found")
|
|
|
|
| 291 |
if result is not None:
|
| 292 |
tool_response = result
|
| 293 |
break
|
| 294 |
+
# Check if confirmation is required
|
| 295 |
+
if tool.requires_confirmation:
|
| 296 |
+
pending = PendingToolCall(
|
| 297 |
+
tool_call=tool_call,
|
| 298 |
+
confirmation_message=tool.get_confirmation_message(
|
| 299 |
+
tool_call.arguments
|
| 300 |
+
)
|
| 301 |
+
)
|
| 302 |
+
pending_calls.append(pending)
|
| 303 |
+
continue
|
| 304 |
+
|
| 305 |
# Stage 2: Execute actual tool only if callback didn't provide a result
|
| 306 |
if tool_response is None:
|
| 307 |
try:
|
|
|
|
| 327 |
break
|
| 328 |
|
| 329 |
results.append(tool_result)
|
| 330 |
+
if pending_calls:
|
| 331 |
+
context.state["pending_tool_calls"] = [p.model_dump() for p in pending_calls]
|
| 332 |
|
| 333 |
return results
|
| 334 |
+
|
| 335 |
+
async def _process_confirmations(
|
| 336 |
+
self,
|
| 337 |
+
context: ExecutionContext
|
| 338 |
+
) -> List[ToolResult]:
|
| 339 |
+
tools_dict = {tool.name: tool for tool in self.tools}
|
| 340 |
+
results = []
|
| 341 |
+
|
| 342 |
+
# Restore pending tool calls from state
|
| 343 |
+
pending_map = {
|
| 344 |
+
p["tool_call"]["tool_call_id"]: PendingToolCall.model_validate(p)
|
| 345 |
+
for p in context.state["pending_tool_calls"]
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
# Build confirmation lookup by tool_call_id
|
| 349 |
+
confirmation_map = {
|
| 350 |
+
c["tool_call_id"]: ToolConfirmation.model_validate(c)
|
| 351 |
+
for c in context.state["tool_confirmations"]
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
# Process ALL pending tool calls
|
| 355 |
+
for tool_call_id, pending in pending_map.items():
|
| 356 |
+
tool = tools_dict.get(pending.tool_call.name)
|
| 357 |
+
confirmation = confirmation_map.get(tool_call_id)
|
| 358 |
+
|
| 359 |
+
if confirmation and confirmation.approved:
|
| 360 |
+
# Merge original arguments with modifications
|
| 361 |
+
arguments = {
|
| 362 |
+
**pending.tool_call.arguments,
|
| 363 |
+
**(confirmation.modified_arguments or {})
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
# Execute the approved tool
|
| 367 |
+
try:
|
| 368 |
+
output = await tool(context, **arguments)
|
| 369 |
+
results.append(ToolResult(
|
| 370 |
+
tool_call_id=tool_call_id,
|
| 371 |
+
name=pending.tool_call.name,
|
| 372 |
+
status="success",
|
| 373 |
+
content=[output],
|
| 374 |
+
))
|
| 375 |
+
except Exception as e:
|
| 376 |
+
results.append(ToolResult(
|
| 377 |
+
tool_call_id=tool_call_id,
|
| 378 |
+
name=pending.tool_call.name,
|
| 379 |
+
status="error",
|
| 380 |
+
content=[str(e)],
|
| 381 |
+
))
|
| 382 |
+
else:
|
| 383 |
+
# Rejected: either explicitly or not in confirmation list
|
| 384 |
+
if confirmation:
|
| 385 |
+
reason = confirmation.reason or "Tool execution was rejected by user."
|
| 386 |
+
else:
|
| 387 |
+
reason = "Tool execution was not approved."
|
| 388 |
+
|
| 389 |
+
results.append(ToolResult(
|
| 390 |
+
tool_call_id=tool_call_id,
|
| 391 |
+
name=pending.tool_call.name,
|
| 392 |
+
status="error",
|
| 393 |
+
content=[reason],
|
| 394 |
+
))
|
| 395 |
+
|
| 396 |
+
return results
|
| 397 |
# List of dangerous tools requiring approval
|
| 398 |
DANGEROUS_TOOLS = ["delete_file", "send_email", "execute_sql"]
|
| 399 |
|
agent_framework/callbacks.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Callback utilities for agent execution."""
|
| 2 |
+
|
| 3 |
+
import inspect
|
| 4 |
+
from typing import Optional, Callable
|
| 5 |
+
|
| 6 |
+
from .models import ExecutionContext
|
| 7 |
+
from .llm import LlmRequest, LlmResponse
|
| 8 |
+
from .memory import count_tokens
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def create_optimizer_callback(
|
| 12 |
+
apply_optimization: Callable,
|
| 13 |
+
threshold: int = 50000,
|
| 14 |
+
model_id: str = "gpt-4"
|
| 15 |
+
) -> Callable:
|
| 16 |
+
"""Factory function that creates a callback applying optimization strategy.
|
| 17 |
+
|
| 18 |
+
Args:
|
| 19 |
+
apply_optimization: Function that modifies the LlmRequest in place
|
| 20 |
+
threshold: Token count threshold to trigger optimization
|
| 21 |
+
model_id: Model identifier for token counting
|
| 22 |
+
|
| 23 |
+
Returns:
|
| 24 |
+
Callback function that can be used as before_llm_callback
|
| 25 |
+
"""
|
| 26 |
+
async def callback(
|
| 27 |
+
context: ExecutionContext,
|
| 28 |
+
request: LlmRequest
|
| 29 |
+
) -> Optional[LlmResponse]:
|
| 30 |
+
token_count = count_tokens(request, model_id=model_id)
|
| 31 |
+
|
| 32 |
+
if token_count < threshold:
|
| 33 |
+
return None
|
| 34 |
+
|
| 35 |
+
# Support both sync and async functions
|
| 36 |
+
result = apply_optimization(context, request)
|
| 37 |
+
if inspect.isawaitable(result):
|
| 38 |
+
await result
|
| 39 |
+
return None
|
| 40 |
+
|
| 41 |
+
return callback
|
| 42 |
+
|
agent_framework/llm.py
CHANGED
|
@@ -8,6 +8,49 @@ from litellm import acompletion
|
|
| 8 |
from .models import Message, ToolCall, ToolResult, ContentItem
|
| 9 |
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
class LlmRequest(BaseModel):
|
| 12 |
"""Request object for LLM calls."""
|
| 13 |
instructions: List[str] = Field(default_factory=list)
|
|
@@ -51,42 +94,7 @@ class LlmClient:
|
|
| 51 |
|
| 52 |
def _build_messages(self, request: LlmRequest) -> List[dict]:
|
| 53 |
"""Convert LlmRequest to API message format."""
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
for instruction in request.instructions:
|
| 57 |
-
messages.append({"role": "system", "content": instruction})
|
| 58 |
-
|
| 59 |
-
for item in request.contents:
|
| 60 |
-
if isinstance(item, Message):
|
| 61 |
-
messages.append({"role": item.role, "content": item.content})
|
| 62 |
-
|
| 63 |
-
elif isinstance(item, ToolCall):
|
| 64 |
-
tool_call_dict = {
|
| 65 |
-
"id": item.tool_call_id,
|
| 66 |
-
"type": "function",
|
| 67 |
-
"function": {
|
| 68 |
-
"name": item.name,
|
| 69 |
-
"arguments": json.dumps(item.arguments)
|
| 70 |
-
}
|
| 71 |
-
}
|
| 72 |
-
# Append to previous assistant message if exists
|
| 73 |
-
if messages and messages[-1]["role"] == "assistant":
|
| 74 |
-
messages[-1].setdefault("tool_calls", []).append(tool_call_dict)
|
| 75 |
-
else:
|
| 76 |
-
messages.append({
|
| 77 |
-
"role": "assistant",
|
| 78 |
-
"content": None,
|
| 79 |
-
"tool_calls": [tool_call_dict]
|
| 80 |
-
})
|
| 81 |
-
|
| 82 |
-
elif isinstance(item, ToolResult):
|
| 83 |
-
messages.append({
|
| 84 |
-
"role": "tool",
|
| 85 |
-
"tool_call_id": item.tool_call_id,
|
| 86 |
-
"content": str(item.content[0]) if item.content else ""
|
| 87 |
-
})
|
| 88 |
-
|
| 89 |
-
return messages
|
| 90 |
|
| 91 |
def _parse_response(self, response) -> LlmResponse:
|
| 92 |
"""Convert API response to LlmResponse."""
|
|
|
|
| 8 |
from .models import Message, ToolCall, ToolResult, ContentItem
|
| 9 |
|
| 10 |
|
| 11 |
+
def build_messages(request: 'LlmRequest') -> List[dict]:
|
| 12 |
+
"""Convert LlmRequest to API message format.
|
| 13 |
+
|
| 14 |
+
Standalone function for use by memory/callback modules.
|
| 15 |
+
"""
|
| 16 |
+
messages = []
|
| 17 |
+
|
| 18 |
+
for instruction in request.instructions:
|
| 19 |
+
messages.append({"role": "system", "content": instruction})
|
| 20 |
+
|
| 21 |
+
for item in request.contents:
|
| 22 |
+
if isinstance(item, Message):
|
| 23 |
+
messages.append({"role": item.role, "content": item.content})
|
| 24 |
+
|
| 25 |
+
elif isinstance(item, ToolCall):
|
| 26 |
+
tool_call_dict = {
|
| 27 |
+
"id": item.tool_call_id,
|
| 28 |
+
"type": "function",
|
| 29 |
+
"function": {
|
| 30 |
+
"name": item.name,
|
| 31 |
+
"arguments": json.dumps(item.arguments)
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
# Append to previous assistant message if exists
|
| 35 |
+
if messages and messages[-1]["role"] == "assistant":
|
| 36 |
+
messages[-1].setdefault("tool_calls", []).append(tool_call_dict)
|
| 37 |
+
else:
|
| 38 |
+
messages.append({
|
| 39 |
+
"role": "assistant",
|
| 40 |
+
"content": None,
|
| 41 |
+
"tool_calls": [tool_call_dict]
|
| 42 |
+
})
|
| 43 |
+
|
| 44 |
+
elif isinstance(item, ToolResult):
|
| 45 |
+
messages.append({
|
| 46 |
+
"role": "tool",
|
| 47 |
+
"tool_call_id": item.tool_call_id,
|
| 48 |
+
"content": str(item.content[0]) if item.content else ""
|
| 49 |
+
})
|
| 50 |
+
|
| 51 |
+
return messages
|
| 52 |
+
|
| 53 |
+
|
| 54 |
class LlmRequest(BaseModel):
|
| 55 |
"""Request object for LLM calls."""
|
| 56 |
instructions: List[str] = Field(default_factory=list)
|
|
|
|
| 94 |
|
| 95 |
def _build_messages(self, request: LlmRequest) -> List[dict]:
|
| 96 |
"""Convert LlmRequest to API message format."""
|
| 97 |
+
return build_messages(request)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
|
| 99 |
def _parse_response(self, response) -> LlmResponse:
|
| 100 |
"""Convert API response to LlmResponse."""
|
agent_framework/memory.py
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Memory optimization strategies for agent conversations."""
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
from typing import Optional, Dict, List
|
| 5 |
+
from .models import ExecutionContext, Message, ToolCall, ToolResult, ContentItem
|
| 6 |
+
from .llm import LlmRequest, LlmResponse, LlmClient, build_messages
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def apply_sliding_window(
|
| 10 |
+
context: ExecutionContext,
|
| 11 |
+
request: LlmRequest,
|
| 12 |
+
window_size: int = 20
|
| 13 |
+
) -> None:
|
| 14 |
+
"""Sliding window that keeps only the most recent N messages"""
|
| 15 |
+
|
| 16 |
+
contents = request.contents
|
| 17 |
+
|
| 18 |
+
# Find user message position
|
| 19 |
+
user_message_idx = None
|
| 20 |
+
for i, item in enumerate(contents):
|
| 21 |
+
if isinstance(item, Message) and item.role == "user":
|
| 22 |
+
user_message_idx = i
|
| 23 |
+
break
|
| 24 |
+
|
| 25 |
+
if user_message_idx is None:
|
| 26 |
+
return
|
| 27 |
+
|
| 28 |
+
# Preserve up to user message
|
| 29 |
+
preserved = contents[:user_message_idx + 1]
|
| 30 |
+
|
| 31 |
+
# Keep only the most recent N from remaining items
|
| 32 |
+
remaining = contents[user_message_idx + 1:]
|
| 33 |
+
if len(remaining) > window_size:
|
| 34 |
+
remaining = remaining[-window_size:]
|
| 35 |
+
|
| 36 |
+
request.contents = preserved + remaining
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def count_tokens(request: LlmRequest, model_id: str = "gpt-4") -> int:
|
| 40 |
+
"""Calculate total token count of LlmRequest.
|
| 41 |
+
|
| 42 |
+
Args:
|
| 43 |
+
request: The LLM request to count tokens for
|
| 44 |
+
model_id: Model identifier for selecting encoding (default: "gpt-4")
|
| 45 |
+
|
| 46 |
+
Returns:
|
| 47 |
+
Estimated total token count
|
| 48 |
+
"""
|
| 49 |
+
import tiktoken
|
| 50 |
+
|
| 51 |
+
# Select encoding for model, use default on failure
|
| 52 |
+
try:
|
| 53 |
+
encoding = tiktoken.encoding_for_model(model_id)
|
| 54 |
+
except KeyError:
|
| 55 |
+
encoding = tiktoken.get_encoding("o200k_base")
|
| 56 |
+
|
| 57 |
+
# Convert to API message format then count tokens
|
| 58 |
+
messages = build_messages(request)
|
| 59 |
+
total_tokens = 0
|
| 60 |
+
|
| 61 |
+
for message in messages:
|
| 62 |
+
# Per-message overhead (role, separators, etc.)
|
| 63 |
+
total_tokens += 4
|
| 64 |
+
|
| 65 |
+
# Content tokens
|
| 66 |
+
if message.get("content"):
|
| 67 |
+
total_tokens += len(encoding.encode(message["content"]))
|
| 68 |
+
|
| 69 |
+
# tool_calls tokens
|
| 70 |
+
if message.get("tool_calls"):
|
| 71 |
+
for tool_call in message["tool_calls"]:
|
| 72 |
+
func = tool_call.get("function", {})
|
| 73 |
+
if func.get("name"):
|
| 74 |
+
total_tokens += len(encoding.encode(func["name"]))
|
| 75 |
+
if func.get("arguments"):
|
| 76 |
+
total_tokens += len(encoding.encode(func["arguments"]))
|
| 77 |
+
|
| 78 |
+
# Tool definition tokens
|
| 79 |
+
if request.tools:
|
| 80 |
+
for tool in request.tools:
|
| 81 |
+
tool_def = tool.tool_definition
|
| 82 |
+
total_tokens += len(encoding.encode(json.dumps(tool_def)))
|
| 83 |
+
|
| 84 |
+
return total_tokens
|
| 85 |
+
|
| 86 |
+
# Tools to compress ToolCall arguments
|
| 87 |
+
TOOLCALL_COMPACTION_RULES = {
|
| 88 |
+
"create_file": "[Content saved to file]",
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
# Tools to compress ToolResult content
|
| 92 |
+
TOOLRESULT_COMPACTION_RULES = {
|
| 93 |
+
"read_file": "File content from {file_path}. Re-read if needed.",
|
| 94 |
+
"search_web": "Search results processed. Query: {query}. Re-search if needed.",
|
| 95 |
+
"tavily_search": "Search results processed. Query: {query}. Re-search if needed.",
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
def apply_compaction(context: ExecutionContext, request: LlmRequest) -> None:
|
| 99 |
+
"""Compress tool calls and results into reference messages"""
|
| 100 |
+
|
| 101 |
+
tool_call_args: Dict[str, Dict] = {}
|
| 102 |
+
compacted = []
|
| 103 |
+
|
| 104 |
+
for item in request.contents:
|
| 105 |
+
if isinstance(item, ToolCall):
|
| 106 |
+
# Save arguments (for use when compressing ToolResult later)
|
| 107 |
+
tool_call_args[item.tool_call_id] = item.arguments
|
| 108 |
+
|
| 109 |
+
# If the ToolCall itself is a compression target (create_file, etc.)
|
| 110 |
+
if item.name in TOOLCALL_COMPACTION_RULES:
|
| 111 |
+
compressed_args = {
|
| 112 |
+
k: TOOLCALL_COMPACTION_RULES[item.name] if k == "content" else v
|
| 113 |
+
for k, v in item.arguments.items()
|
| 114 |
+
}
|
| 115 |
+
compacted.append(ToolCall(
|
| 116 |
+
tool_call_id=item.tool_call_id,
|
| 117 |
+
name=item.name,
|
| 118 |
+
arguments=compressed_args
|
| 119 |
+
))
|
| 120 |
+
else:
|
| 121 |
+
compacted.append(item)
|
| 122 |
+
|
| 123 |
+
elif isinstance(item, ToolResult):
|
| 124 |
+
# If ToolResult is a compression target (read_file, search_web, etc.)
|
| 125 |
+
if item.name in TOOLRESULT_COMPACTION_RULES:
|
| 126 |
+
args = tool_call_args.get(item.tool_call_id, {})
|
| 127 |
+
template = TOOLRESULT_COMPACTION_RULES[item.name]
|
| 128 |
+
compressed_content = template.format(
|
| 129 |
+
file_path=args.get("file_path", args.get("path", "unknown")),
|
| 130 |
+
query=args.get("query", "unknown")
|
| 131 |
+
)
|
| 132 |
+
compacted.append(ToolResult(
|
| 133 |
+
tool_call_id=item.tool_call_id,
|
| 134 |
+
name=item.name,
|
| 135 |
+
status=item.status,
|
| 136 |
+
content=[compressed_content]
|
| 137 |
+
))
|
| 138 |
+
else:
|
| 139 |
+
compacted.append(item)
|
| 140 |
+
else:
|
| 141 |
+
compacted.append(item)
|
| 142 |
+
|
| 143 |
+
request.contents = compacted
|
| 144 |
+
|
| 145 |
+
SUMMARIZATION_PROMPT = """You are summarizing an AI agent's work progress.
|
| 146 |
+
|
| 147 |
+
Given the following execution history, extract:
|
| 148 |
+
1. Key findings: Important information discovered
|
| 149 |
+
2. Tools used: List of tools that were called
|
| 150 |
+
3. Current status: What has been accomplished and what remains
|
| 151 |
+
|
| 152 |
+
Be concise. Focus on information that will help the agent continue its work.
|
| 153 |
+
|
| 154 |
+
Execution History:
|
| 155 |
+
{history}
|
| 156 |
+
|
| 157 |
+
Provide a structured summary."""
|
| 158 |
+
|
| 159 |
+
async def apply_summarization(
|
| 160 |
+
context: ExecutionContext,
|
| 161 |
+
request: LlmRequest,
|
| 162 |
+
llm_client: LlmClient,
|
| 163 |
+
keep_recent: int = 5
|
| 164 |
+
) -> None:
|
| 165 |
+
"""Replace old messages with a summary"""
|
| 166 |
+
|
| 167 |
+
contents = request.contents
|
| 168 |
+
|
| 169 |
+
# Find user message position
|
| 170 |
+
user_idx = None
|
| 171 |
+
for i, item in enumerate(contents):
|
| 172 |
+
if isinstance(item, Message) and item.role == "user":
|
| 173 |
+
user_idx = i
|
| 174 |
+
break
|
| 175 |
+
|
| 176 |
+
if user_idx is None:
|
| 177 |
+
return
|
| 178 |
+
|
| 179 |
+
# Check previous summary position (skip already-summarized portions)
|
| 180 |
+
last_summary_idx = context.state.get("last_summary_idx", user_idx)
|
| 181 |
+
|
| 182 |
+
# Calculate summarization target range
|
| 183 |
+
summary_start = last_summary_idx + 1
|
| 184 |
+
summary_end = len(contents) - keep_recent
|
| 185 |
+
|
| 186 |
+
# Overlap prevention: exit if nothing to summarize or range is invalid
|
| 187 |
+
if summary_end <= summary_start:
|
| 188 |
+
return
|
| 189 |
+
|
| 190 |
+
# Determine portions to preserve (no overlap)
|
| 191 |
+
preserved_start = contents[:last_summary_idx + 1]
|
| 192 |
+
preserved_end = contents[summary_end:]
|
| 193 |
+
to_summarize = contents[summary_start:summary_end]
|
| 194 |
+
|
| 195 |
+
# Generate summary
|
| 196 |
+
history_text = format_history_for_summary(to_summarize)
|
| 197 |
+
summary = await generate_summary(llm_client, history_text)
|
| 198 |
+
|
| 199 |
+
# Add summary to instructions
|
| 200 |
+
request.append_instructions(f"[Previous work summary]\n{summary}")
|
| 201 |
+
|
| 202 |
+
# Keep only preserved portions in contents
|
| 203 |
+
request.contents = preserved_start + preserved_end
|
| 204 |
+
|
| 205 |
+
# Record summary position
|
| 206 |
+
context.state["last_summary_idx"] = len(preserved_start) - 1
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
def format_history_for_summary(items: List[ContentItem]) -> str:
|
| 210 |
+
"""Convert ContentItem list to text for summarization"""
|
| 211 |
+
lines = []
|
| 212 |
+
for item in items:
|
| 213 |
+
if isinstance(item, Message):
|
| 214 |
+
lines.append(f"[{item.role}]: {item.content[:500]}...")
|
| 215 |
+
elif isinstance(item, ToolCall):
|
| 216 |
+
lines.append(f"[Tool Call]: {item.name}({item.arguments})")
|
| 217 |
+
elif isinstance(item, ToolResult):
|
| 218 |
+
content_preview = str(item.content[0])[:200] if item.content else ""
|
| 219 |
+
lines.append(f"[Tool Result]: {item.name} -> {content_preview}...")
|
| 220 |
+
return "\n".join(lines)
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
async def generate_summary(llm_client: LlmClient, history: str) -> str:
|
| 224 |
+
"""Generate history summary using LLM"""
|
| 225 |
+
|
| 226 |
+
request = LlmRequest(
|
| 227 |
+
instructions=[SUMMARIZATION_PROMPT.format(history=history)],
|
| 228 |
+
contents=[Message(role="user", content="Please summarize.")]
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
+
response = await llm_client.generate(request)
|
| 232 |
+
|
| 233 |
+
for item in response.content:
|
| 234 |
+
if isinstance(item, Message):
|
| 235 |
+
return item.content
|
| 236 |
+
|
| 237 |
+
return ""
|
| 238 |
+
|
| 239 |
+
class ContextOptimizer:
|
| 240 |
+
"""Hierarchical context optimization strategy"""
|
| 241 |
+
|
| 242 |
+
def __init__(
|
| 243 |
+
self,
|
| 244 |
+
llm_client: LlmClient,
|
| 245 |
+
token_threshold: int = 50000,
|
| 246 |
+
enable_compaction: bool = True,
|
| 247 |
+
enable_summarization: bool = True,
|
| 248 |
+
keep_recent: int = 5
|
| 249 |
+
):
|
| 250 |
+
self.llm_client = llm_client
|
| 251 |
+
self.token_threshold = token_threshold
|
| 252 |
+
self.enable_compaction = enable_compaction
|
| 253 |
+
self.enable_summarization = enable_summarization
|
| 254 |
+
self.keep_recent = keep_recent
|
| 255 |
+
|
| 256 |
+
async def __call__(
|
| 257 |
+
self,
|
| 258 |
+
context: ExecutionContext,
|
| 259 |
+
request: LlmRequest
|
| 260 |
+
) -> Optional[LlmResponse]:
|
| 261 |
+
"""Register as before_llm_callback"""
|
| 262 |
+
|
| 263 |
+
# Step 1: Measure tokens
|
| 264 |
+
if count_tokens(request) < self.token_threshold:
|
| 265 |
+
return None
|
| 266 |
+
|
| 267 |
+
# Step 2: Apply Compaction
|
| 268 |
+
if self.enable_compaction:
|
| 269 |
+
apply_compaction(context, request)
|
| 270 |
+
|
| 271 |
+
if count_tokens(request) < self.token_threshold:
|
| 272 |
+
return None
|
| 273 |
+
|
| 274 |
+
# Step 3: Apply Summarization
|
| 275 |
+
if self.enable_summarization:
|
| 276 |
+
await apply_summarization(
|
| 277 |
+
context,
|
| 278 |
+
request,
|
| 279 |
+
self.llm_client,
|
| 280 |
+
self.keep_recent
|
| 281 |
+
)
|
| 282 |
+
|
| 283 |
+
return None
|
agent_framework/models.py
CHANGED
|
@@ -33,6 +33,19 @@ class ToolResult(BaseModel):
|
|
| 33 |
|
| 34 |
ContentItem = Union[Message, ToolCall, ToolResult]
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
class Event(BaseModel):
|
| 38 |
"""A recorded occurrence during agent execution."""
|
|
@@ -52,6 +65,7 @@ class ExecutionContext:
|
|
| 52 |
current_step: int = 0
|
| 53 |
state: Dict[str, Any] = field(default_factory=dict)
|
| 54 |
final_result: Optional[str | BaseModel] = None
|
|
|
|
| 55 |
|
| 56 |
def add_event(self, event: Event):
|
| 57 |
"""Append an event to the execution history."""
|
|
@@ -60,3 +74,78 @@ class ExecutionContext:
|
|
| 60 |
def increment_step(self):
|
| 61 |
"""Move to the next execution step."""
|
| 62 |
self.current_step += 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
ContentItem = Union[Message, ToolCall, ToolResult]
|
| 35 |
|
| 36 |
+
class ToolConfirmation(BaseModel):
|
| 37 |
+
"""User's decision on a pending tool call."""
|
| 38 |
+
|
| 39 |
+
tool_call_id: str
|
| 40 |
+
approved: bool
|
| 41 |
+
modified_arguments: dict | None = None
|
| 42 |
+
reason: str | None = None # Reason for rejection (if not approved)
|
| 43 |
+
|
| 44 |
+
class PendingToolCall(BaseModel):
|
| 45 |
+
"""A tool call awaiting user confirmation."""
|
| 46 |
+
|
| 47 |
+
tool_call: ToolCall
|
| 48 |
+
confirmation_message: str
|
| 49 |
|
| 50 |
class Event(BaseModel):
|
| 51 |
"""A recorded occurrence during agent execution."""
|
|
|
|
| 65 |
current_step: int = 0
|
| 66 |
state: Dict[str, Any] = field(default_factory=dict)
|
| 67 |
final_result: Optional[str | BaseModel] = None
|
| 68 |
+
session_id: Optional[str] = None # Link to session for persistence
|
| 69 |
|
| 70 |
def add_event(self, event: Event):
|
| 71 |
"""Append an event to the execution history."""
|
|
|
|
| 74 |
def increment_step(self):
|
| 75 |
"""Move to the next execution step."""
|
| 76 |
self.current_step += 1
|
| 77 |
+
|
| 78 |
+
class Session(BaseModel):
|
| 79 |
+
"""Container for persistent conversation state across multiple run() calls."""
|
| 80 |
+
|
| 81 |
+
session_id: str
|
| 82 |
+
user_id: str | None = None
|
| 83 |
+
events: list[Event] = Field(default_factory=list)
|
| 84 |
+
state: dict[str, Any] = Field(default_factory=dict)
|
| 85 |
+
created_at: datetime = Field(default_factory=datetime.now)
|
| 86 |
+
updated_at: datetime = Field(default_factory=datetime.now)
|
| 87 |
+
|
| 88 |
+
from abc import ABC, abstractmethod
|
| 89 |
+
|
| 90 |
+
class BaseSessionManager(ABC):
|
| 91 |
+
"""Abstract base class for session management."""
|
| 92 |
+
|
| 93 |
+
@abstractmethod
|
| 94 |
+
async def create(
|
| 95 |
+
self,
|
| 96 |
+
session_id: str,
|
| 97 |
+
user_id: str | None = None
|
| 98 |
+
) -> Session:
|
| 99 |
+
"""Create a new session."""
|
| 100 |
+
pass
|
| 101 |
+
|
| 102 |
+
@abstractmethod
|
| 103 |
+
async def get(self, session_id: str) -> Session | None:
|
| 104 |
+
"""Retrieve a session by ID. Returns None if not found."""
|
| 105 |
+
pass
|
| 106 |
+
|
| 107 |
+
@abstractmethod
|
| 108 |
+
async def save(self, session: Session) -> None:
|
| 109 |
+
"""Persist session changes to storage."""
|
| 110 |
+
pass
|
| 111 |
+
|
| 112 |
+
async def get_or_create(
|
| 113 |
+
self,
|
| 114 |
+
session_id: str,
|
| 115 |
+
user_id: str | None = None
|
| 116 |
+
) -> Session:
|
| 117 |
+
"""Get existing session or create new one."""
|
| 118 |
+
session = await self.get(session_id)
|
| 119 |
+
if session is None:
|
| 120 |
+
session = await self.create(session_id, user_id)
|
| 121 |
+
return session
|
| 122 |
+
|
| 123 |
+
class InMemorySessionManager(BaseSessionManager):
|
| 124 |
+
"""In-memory session storage for development and testing."""
|
| 125 |
+
|
| 126 |
+
def __init__(self):
|
| 127 |
+
self._sessions: dict[str, Session] = {}
|
| 128 |
+
|
| 129 |
+
async def create(
|
| 130 |
+
self,
|
| 131 |
+
session_id: str,
|
| 132 |
+
user_id: str | None = None
|
| 133 |
+
) -> Session:
|
| 134 |
+
"""Create a new session."""
|
| 135 |
+
if session_id in self._sessions:
|
| 136 |
+
raise ValueError(f"Session {session_id} already exists")
|
| 137 |
+
|
| 138 |
+
session = Session(
|
| 139 |
+
session_id=session_id,
|
| 140 |
+
user_id=user_id
|
| 141 |
+
)
|
| 142 |
+
self._sessions[session_id] = session
|
| 143 |
+
return session
|
| 144 |
+
|
| 145 |
+
async def get(self, session_id: str) -> Session | None:
|
| 146 |
+
"""Retrieve a session by ID."""
|
| 147 |
+
return self._sessions.get(session_id)
|
| 148 |
+
|
| 149 |
+
async def save(self, session: Session) -> None:
|
| 150 |
+
"""Save session to storage."""
|
| 151 |
+
self._sessions[session.session_id] = session
|
agent_framework/tools.py
CHANGED
|
@@ -15,10 +15,18 @@ class BaseTool(ABC):
|
|
| 15 |
name: str = None,
|
| 16 |
description: str = None,
|
| 17 |
tool_definition: Dict[str, Any] = None,
|
|
|
|
|
|
|
|
|
|
| 18 |
):
|
| 19 |
self.name = name or self.__class__.__name__
|
| 20 |
self.description = description or self.__doc__ or ""
|
| 21 |
self._tool_definition = tool_definition
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
@property
|
| 24 |
def tool_definition(self) -> Dict[str, Any] | None:
|
|
@@ -31,7 +39,12 @@ class BaseTool(ABC):
|
|
| 31 |
async def __call__(self, context: ExecutionContext, **kwargs) -> Any:
|
| 32 |
return await self.execute(context, **kwargs)
|
| 33 |
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
class FunctionTool(BaseTool):
|
| 36 |
"""Wraps a Python function as a BaseTool."""
|
| 37 |
|
|
@@ -40,7 +53,9 @@ class FunctionTool(BaseTool):
|
|
| 40 |
func: Callable,
|
| 41 |
name: str = None,
|
| 42 |
description: str = None,
|
| 43 |
-
tool_definition: Dict[str, Any] = None
|
|
|
|
|
|
|
| 44 |
):
|
| 45 |
self.func = func
|
| 46 |
self.needs_context = 'context' in inspect.signature(func).parameters
|
|
@@ -52,7 +67,9 @@ class FunctionTool(BaseTool):
|
|
| 52 |
super().__init__(
|
| 53 |
name=self.name,
|
| 54 |
description=self.description,
|
| 55 |
-
tool_definition=tool_definition
|
|
|
|
|
|
|
| 56 |
)
|
| 57 |
|
| 58 |
async def execute(self, context: ExecutionContext = None, **kwargs) -> Any:
|
|
@@ -86,7 +103,9 @@ def tool(
|
|
| 86 |
*,
|
| 87 |
name: str = None,
|
| 88 |
description: str = None,
|
| 89 |
-
tool_definition: Dict[str, Any] = None
|
|
|
|
|
|
|
| 90 |
):
|
| 91 |
"""Decorator to convert a function into a FunctionTool.
|
| 92 |
|
|
@@ -99,6 +118,11 @@ def tool(
|
|
| 99 |
@tool(name="custom_name", description="Custom description")
|
| 100 |
def my_function(x: int) -> int:
|
| 101 |
return x * 2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
"""
|
| 103 |
from typing import Union
|
| 104 |
|
|
@@ -107,9 +131,12 @@ def tool(
|
|
| 107 |
func=f,
|
| 108 |
name=name,
|
| 109 |
description=description,
|
| 110 |
-
tool_definition=tool_definition
|
|
|
|
|
|
|
| 111 |
)
|
| 112 |
|
| 113 |
if func is not None:
|
| 114 |
return decorator(func)
|
| 115 |
-
return decorator
|
|
|
|
|
|
| 15 |
name: str = None,
|
| 16 |
description: str = None,
|
| 17 |
tool_definition: Dict[str, Any] = None,
|
| 18 |
+
# Confirmation support
|
| 19 |
+
requires_confirmation: bool = False,
|
| 20 |
+
confirmation_message_template: str = None
|
| 21 |
):
|
| 22 |
self.name = name or self.__class__.__name__
|
| 23 |
self.description = description or self.__doc__ or ""
|
| 24 |
self._tool_definition = tool_definition
|
| 25 |
+
self.requires_confirmation = requires_confirmation
|
| 26 |
+
self.confirmation_message_template = confirmation_message_template or (
|
| 27 |
+
"The agent wants to execute '{name}' with arguments: {arguments}. "
|
| 28 |
+
"Do you approve?"
|
| 29 |
+
)
|
| 30 |
|
| 31 |
@property
|
| 32 |
def tool_definition(self) -> Dict[str, Any] | None:
|
|
|
|
| 39 |
async def __call__(self, context: ExecutionContext, **kwargs) -> Any:
|
| 40 |
return await self.execute(context, **kwargs)
|
| 41 |
|
| 42 |
+
def get_confirmation_message(self, arguments: dict[str, Any]) -> str:
|
| 43 |
+
"""Generate a confirmation message for this tool call."""
|
| 44 |
+
return self.confirmation_message_template.format(
|
| 45 |
+
name=self.name,
|
| 46 |
+
arguments=arguments
|
| 47 |
+
)
|
| 48 |
class FunctionTool(BaseTool):
|
| 49 |
"""Wraps a Python function as a BaseTool."""
|
| 50 |
|
|
|
|
| 53 |
func: Callable,
|
| 54 |
name: str = None,
|
| 55 |
description: str = None,
|
| 56 |
+
tool_definition: Dict[str, Any] = None,
|
| 57 |
+
requires_confirmation: bool = False,
|
| 58 |
+
confirmation_message_template: str = None
|
| 59 |
):
|
| 60 |
self.func = func
|
| 61 |
self.needs_context = 'context' in inspect.signature(func).parameters
|
|
|
|
| 67 |
super().__init__(
|
| 68 |
name=self.name,
|
| 69 |
description=self.description,
|
| 70 |
+
tool_definition=tool_definition,
|
| 71 |
+
requires_confirmation=requires_confirmation,
|
| 72 |
+
confirmation_message_template=confirmation_message_template
|
| 73 |
)
|
| 74 |
|
| 75 |
async def execute(self, context: ExecutionContext = None, **kwargs) -> Any:
|
|
|
|
| 103 |
*,
|
| 104 |
name: str = None,
|
| 105 |
description: str = None,
|
| 106 |
+
tool_definition: Dict[str, Any] = None,
|
| 107 |
+
requires_confirmation: bool = False,
|
| 108 |
+
confirmation_message: str = None
|
| 109 |
):
|
| 110 |
"""Decorator to convert a function into a FunctionTool.
|
| 111 |
|
|
|
|
| 118 |
@tool(name="custom_name", description="Custom description")
|
| 119 |
def my_function(x: int) -> int:
|
| 120 |
return x * 2
|
| 121 |
+
|
| 122 |
+
# With confirmation:
|
| 123 |
+
@tool(requires_confirmation=True, confirmation_message="Delete file?")
|
| 124 |
+
def delete_file(filename: str) -> str:
|
| 125 |
+
...
|
| 126 |
"""
|
| 127 |
from typing import Union
|
| 128 |
|
|
|
|
| 131 |
func=f,
|
| 132 |
name=name,
|
| 133 |
description=description,
|
| 134 |
+
tool_definition=tool_definition,
|
| 135 |
+
requires_confirmation=requires_confirmation,
|
| 136 |
+
confirmation_message_template=confirmation_message
|
| 137 |
)
|
| 138 |
|
| 139 |
if func is not None:
|
| 140 |
return decorator(func)
|
| 141 |
+
return decorator
|
| 142 |
+
|
agent_framework/utils.py
CHANGED
|
@@ -79,41 +79,57 @@ def mcp_tools_to_openai_format(mcp_tools) -> list[dict]:
|
|
| 79 |
]
|
| 80 |
|
| 81 |
|
| 82 |
-
def
|
| 83 |
-
"""
|
| 84 |
|
| 85 |
Args:
|
| 86 |
-
context: ExecutionContext to
|
|
|
|
|
|
|
|
|
|
| 87 |
"""
|
| 88 |
-
from .models import
|
| 89 |
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
|
|
|
|
|
|
| 93 |
|
| 94 |
for i, event in enumerate(context.events, 1):
|
| 95 |
-
|
| 96 |
-
|
| 97 |
|
| 98 |
for item in event.content:
|
| 99 |
if isinstance(item, Message):
|
| 100 |
content_preview = item.content[:100] + "..." if len(item.content) > 100 else item.content
|
| 101 |
-
|
| 102 |
elif isinstance(item, ToolCall):
|
| 103 |
-
|
| 104 |
-
|
| 105 |
elif isinstance(item, ToolResult):
|
| 106 |
status_marker = "[SUCCESS]" if item.status == "success" else "[ERROR]"
|
| 107 |
-
|
| 108 |
if item.content:
|
| 109 |
content_preview = str(item.content[0])[:100]
|
| 110 |
if len(str(item.content[0])) > 100:
|
| 111 |
content_preview += "..."
|
| 112 |
-
|
| 113 |
|
| 114 |
-
|
| 115 |
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
]
|
| 80 |
|
| 81 |
|
| 82 |
+
def format_trace(context) -> str:
|
| 83 |
+
"""Format execution trace as a string.
|
| 84 |
|
| 85 |
Args:
|
| 86 |
+
context: ExecutionContext to format
|
| 87 |
+
|
| 88 |
+
Returns:
|
| 89 |
+
Formatted trace string
|
| 90 |
"""
|
| 91 |
+
from .models import Message, ToolCall, ToolResult
|
| 92 |
|
| 93 |
+
lines = []
|
| 94 |
+
lines.append("=" * 60)
|
| 95 |
+
lines.append(f"Execution Trace (ID: {context.execution_id})")
|
| 96 |
+
lines.append("=" * 60)
|
| 97 |
+
lines.append("")
|
| 98 |
|
| 99 |
for i, event in enumerate(context.events, 1):
|
| 100 |
+
lines.append(f"Step {i} - {event.author.upper()} ({event.timestamp:.2f})")
|
| 101 |
+
lines.append("-" * 60)
|
| 102 |
|
| 103 |
for item in event.content:
|
| 104 |
if isinstance(item, Message):
|
| 105 |
content_preview = item.content[:100] + "..." if len(item.content) > 100 else item.content
|
| 106 |
+
lines.append(f" [Message] ({item.role}): {content_preview}")
|
| 107 |
elif isinstance(item, ToolCall):
|
| 108 |
+
lines.append(f" [Tool Call] {item.name}")
|
| 109 |
+
lines.append(f" Arguments: {item.arguments}")
|
| 110 |
elif isinstance(item, ToolResult):
|
| 111 |
status_marker = "[SUCCESS]" if item.status == "success" else "[ERROR]"
|
| 112 |
+
lines.append(f" {status_marker} Tool Result: {item.name} ({item.status})")
|
| 113 |
if item.content:
|
| 114 |
content_preview = str(item.content[0])[:100]
|
| 115 |
if len(str(item.content[0])) > 100:
|
| 116 |
content_preview += "..."
|
| 117 |
+
lines.append(f" Output: {content_preview}")
|
| 118 |
|
| 119 |
+
lines.append("")
|
| 120 |
|
| 121 |
+
lines.append("=" * 60)
|
| 122 |
+
lines.append(f"Final Result: {context.final_result}")
|
| 123 |
+
lines.append(f"Total Steps: {context.current_step}")
|
| 124 |
+
lines.append("=" * 60)
|
| 125 |
+
|
| 126 |
+
return "\n".join(lines)
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def display_trace(context):
|
| 130 |
+
"""Display the execution trace of an agent run.
|
| 131 |
+
|
| 132 |
+
Args:
|
| 133 |
+
context: ExecutionContext to display
|
| 134 |
+
"""
|
| 135 |
+
print(format_trace(context))
|
agent_tools/example_usage.py
CHANGED
|
@@ -20,13 +20,13 @@ async def main():
|
|
| 20 |
agent = Agent(
|
| 21 |
model=LlmClient(model="gpt-5-mini"), # Use a valid model name
|
| 22 |
tools=[search_web, list_files, read_file, delete_file],
|
| 23 |
-
instructions="You are a helpful assistant that
|
| 24 |
max_steps=20,
|
| 25 |
before_tool_callbacks=[approval_callback],
|
| 26 |
after_tool_callbacks=[search_compressor],
|
| 27 |
)
|
| 28 |
|
| 29 |
-
result = await agent.run("
|
| 30 |
print(result.output)
|
| 31 |
|
| 32 |
if __name__ == "__main__":
|
|
|
|
| 20 |
agent = Agent(
|
| 21 |
model=LlmClient(model="gpt-5-mini"), # Use a valid model name
|
| 22 |
tools=[search_web, list_files, read_file, delete_file],
|
| 23 |
+
instructions="You are a helpful assistant that can search the web and explore files to answer questions.",
|
| 24 |
max_steps=20,
|
| 25 |
before_tool_callbacks=[approval_callback],
|
| 26 |
after_tool_callbacks=[search_compressor],
|
| 27 |
)
|
| 28 |
|
| 29 |
+
result = await agent.run("search about andrej karpathy")
|
| 30 |
print(result.output)
|
| 31 |
|
| 32 |
if __name__ == "__main__":
|
agent_tools/file_tools.py
CHANGED
|
@@ -319,8 +319,15 @@ def _analyze_pdf(file_path: str, query: str) -> str:
|
|
| 319 |
)
|
| 320 |
return response.choices[0].message.content
|
| 321 |
|
| 322 |
-
@tool
|
| 323 |
-
|
| 324 |
-
"
|
| 325 |
-
|
| 326 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
)
|
| 320 |
return response.choices[0].message.content
|
| 321 |
|
| 322 |
+
@tool(
|
| 323 |
+
name="delete_file",
|
| 324 |
+
description="Delete a file from the filesystem",
|
| 325 |
+
requires_confirmation=True,
|
| 326 |
+
confirmation_message="The agent wants to delete a file. Arguments: {arguments}. "
|
| 327 |
+
"This action cannot be undone. Do you approve?"
|
| 328 |
+
)
|
| 329 |
+
def delete_file(filename: str) -> str:
|
| 330 |
+
"""Delete the specified file."""
|
| 331 |
+
import os
|
| 332 |
+
os.remove(filename)
|
| 333 |
+
return f"Successfully deleted {filename}"
|
agent_tools/web_tools.py
CHANGED
|
@@ -75,24 +75,62 @@ def _extract_search_query(context: ExecutionContext, tool_call_id: str) -> str:
|
|
| 75 |
return item.arguments.get("query", "")
|
| 76 |
return ""
|
| 77 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
## callbacks
|
| 79 |
def search_compressor(context: ExecutionContext, tool_result: ToolResult):
|
| 80 |
"""Callback that compresses web search results."""
|
| 81 |
# Pass through unchanged if not a search tool
|
| 82 |
if tool_result.name != "search_web":
|
|
|
|
| 83 |
return None
|
| 84 |
|
| 85 |
original_content = tool_result.content[0]
|
|
|
|
| 86 |
|
| 87 |
# No compression needed if result is short enough
|
| 88 |
if len(original_content) < 2000:
|
|
|
|
| 89 |
return None
|
| 90 |
|
| 91 |
# Extract search query matching the tool_call_id
|
| 92 |
query = _extract_search_query(context, tool_result.tool_call_id)
|
| 93 |
if not query:
|
|
|
|
| 94 |
return None
|
| 95 |
|
|
|
|
| 96 |
# Use functions implemented in section 5.3
|
| 97 |
chunks = fixed_length_chunking(original_content, chunk_size=500, overlap=50)
|
| 98 |
embeddings = get_embeddings(chunks)
|
|
@@ -100,6 +138,7 @@ def search_compressor(context: ExecutionContext, tool_result: ToolResult):
|
|
| 100 |
|
| 101 |
# Create compressed result
|
| 102 |
compressed = "\n\n".join([r['chunk'] for r in results])
|
|
|
|
| 103 |
|
| 104 |
return ToolResult(
|
| 105 |
tool_call_id=tool_result.tool_call_id,
|
|
|
|
| 75 |
return item.arguments.get("query", "")
|
| 76 |
return ""
|
| 77 |
|
| 78 |
+
## callbacks
|
| 79 |
+
# def search_compressor(context: ExecutionContext, tool_result: ToolResult):
|
| 80 |
+
# """Callback that compresses web search results."""
|
| 81 |
+
# # Pass through unchanged if not a search tool
|
| 82 |
+
# if tool_result.name != "search_web":
|
| 83 |
+
# return None
|
| 84 |
+
|
| 85 |
+
# original_content = tool_result.content[0]
|
| 86 |
+
|
| 87 |
+
# # No compression needed if result is short enough
|
| 88 |
+
# if len(original_content) < 2000:
|
| 89 |
+
# return None
|
| 90 |
+
|
| 91 |
+
# # Extract search query matching the tool_call_id
|
| 92 |
+
# query = _extract_search_query(context, tool_result.tool_call_id)
|
| 93 |
+
# if not query:
|
| 94 |
+
# return None
|
| 95 |
+
|
| 96 |
+
# # Use functions implemented in section 5.3
|
| 97 |
+
# chunks = fixed_length_chunking(original_content, chunk_size=500, overlap=50)
|
| 98 |
+
# embeddings = get_embeddings(chunks)
|
| 99 |
+
# results = vector_search(query, chunks, embeddings, top_k=3)
|
| 100 |
+
|
| 101 |
+
# # Create compressed result
|
| 102 |
+
# compressed = "\n\n".join([r['chunk'] for r in results])
|
| 103 |
+
|
| 104 |
+
# return ToolResult(
|
| 105 |
+
# tool_call_id=tool_result.tool_call_id,
|
| 106 |
+
# name=tool_result.name,
|
| 107 |
+
# status="success",
|
| 108 |
+
# content=[compressed]
|
| 109 |
+
# )
|
| 110 |
+
|
| 111 |
## callbacks
|
| 112 |
def search_compressor(context: ExecutionContext, tool_result: ToolResult):
|
| 113 |
"""Callback that compresses web search results."""
|
| 114 |
# Pass through unchanged if not a search tool
|
| 115 |
if tool_result.name != "search_web":
|
| 116 |
+
print("DEBUG: Callback skipped - not a search_web tool")
|
| 117 |
return None
|
| 118 |
|
| 119 |
original_content = tool_result.content[0]
|
| 120 |
+
print(f"DEBUG: Callback triggered! Original content length: {len(original_content)}")
|
| 121 |
|
| 122 |
# No compression needed if result is short enough
|
| 123 |
if len(original_content) < 2000:
|
| 124 |
+
print("DEBUG: Callback skipped - content too short")
|
| 125 |
return None
|
| 126 |
|
| 127 |
# Extract search query matching the tool_call_id
|
| 128 |
query = _extract_search_query(context, tool_result.tool_call_id)
|
| 129 |
if not query:
|
| 130 |
+
print("DEBUG: Callback skipped - could not extract query")
|
| 131 |
return None
|
| 132 |
|
| 133 |
+
print(f"DEBUG: Compressing search results for query: {query}")
|
| 134 |
# Use functions implemented in section 5.3
|
| 135 |
chunks = fixed_length_chunking(original_content, chunk_size=500, overlap=50)
|
| 136 |
embeddings = get_embeddings(chunks)
|
|
|
|
| 138 |
|
| 139 |
# Create compressed result
|
| 140 |
compressed = "\n\n".join([r['chunk'] for r in results])
|
| 141 |
+
print(f"DEBUG: Compressed from {len(original_content)} to {len(compressed)} chars")
|
| 142 |
|
| 143 |
return ToolResult(
|
| 144 |
tool_call_id=tool_result.tool_call_id,
|
example_agent.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Simple example to test the agent framework.
|
| 2 |
+
|
| 3 |
+
This script demonstrates basic agent usage with tools.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import asyncio
|
| 7 |
+
import sys
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
# Add parent directory to path
|
| 11 |
+
sys.path.insert(0, str(Path(__file__).parent))
|
| 12 |
+
|
| 13 |
+
from agent_framework import Agent, LlmClient, display_trace
|
| 14 |
+
from agent_tools import calculator, search_web
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
async def main():
|
| 18 |
+
"""Run a simple agent example."""
|
| 19 |
+
|
| 20 |
+
print("=" * 60)
|
| 21 |
+
print("Agent Framework - Simple Test")
|
| 22 |
+
print("=" * 60)
|
| 23 |
+
print()
|
| 24 |
+
|
| 25 |
+
# Create agent with calculator and web search tools
|
| 26 |
+
agent = Agent(
|
| 27 |
+
model=LlmClient(model="gpt-4o-mini"), # Use a cost-effective model for testing
|
| 28 |
+
tools=[calculator, search_web],
|
| 29 |
+
instructions="You are a helpful assistant. Use websearch tool to search web for sure.",
|
| 30 |
+
max_steps=10
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
result1 = await agent.run("What are the finalists of australian open 2026 mens singles")
|
| 34 |
+
print(f"\nAnswer: {result1.output}")
|
| 35 |
+
print(f"Steps taken: {result1.context}")
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
if __name__ == "__main__":
|
| 40 |
+
asyncio.run(main())
|
| 41 |
+
|
pyproject.toml
CHANGED
|
@@ -7,6 +7,7 @@ requires-python = ">=3.11"
|
|
| 7 |
dependencies = [
|
| 8 |
"chromadb>=1.0.20",
|
| 9 |
"datasets>=4.5.0",
|
|
|
|
| 10 |
"fastmcp>=2.11.3",
|
| 11 |
"ipykernel>=7.1.0",
|
| 12 |
"litellm>=1.81.3",
|
|
@@ -17,9 +18,11 @@ dependencies = [
|
|
| 17 |
"pydantic>=2.11.7",
|
| 18 |
"pymupdf>=1.26.7",
|
| 19 |
"python-dotenv>=1.1.1",
|
|
|
|
| 20 |
"scikit-learn>=1.0.0",
|
| 21 |
"tavily-python>=0.7.11",
|
| 22 |
"tqdm>=4.67.1",
|
|
|
|
| 23 |
"wikipedia>=1.4.0",
|
| 24 |
]
|
| 25 |
|
|
|
|
| 7 |
dependencies = [
|
| 8 |
"chromadb>=1.0.20",
|
| 9 |
"datasets>=4.5.0",
|
| 10 |
+
"fastapi>=0.100.0",
|
| 11 |
"fastmcp>=2.11.3",
|
| 12 |
"ipykernel>=7.1.0",
|
| 13 |
"litellm>=1.81.3",
|
|
|
|
| 18 |
"pydantic>=2.11.7",
|
| 19 |
"pymupdf>=1.26.7",
|
| 20 |
"python-dotenv>=1.1.1",
|
| 21 |
+
"python-multipart>=0.0.6",
|
| 22 |
"scikit-learn>=1.0.0",
|
| 23 |
"tavily-python>=0.7.11",
|
| 24 |
"tqdm>=4.67.1",
|
| 25 |
+
"uvicorn>=0.23.0",
|
| 26 |
"wikipedia>=1.4.0",
|
| 27 |
]
|
| 28 |
|
rag/embeddings.py
CHANGED
|
@@ -41,8 +41,4 @@ sentences = [
|
|
| 41 |
embeddings = get_embeddings(sentences)
|
| 42 |
|
| 43 |
cat_kitten = cosine_similarity([embeddings[0]], [embeddings[1]])[0][0]
|
| 44 |
-
cat_dog = cosine_similarity([embeddings[0]], [embeddings[2]])[0][0]
|
| 45 |
-
|
| 46 |
-
print(f"Cat vs Kitten: {cat_kitten:.3f}")
|
| 47 |
-
print(f"Cat vs Dog: {cat_dog:.3f}")
|
| 48 |
-
|
|
|
|
| 41 |
embeddings = get_embeddings(sentences)
|
| 42 |
|
| 43 |
cat_kitten = cosine_similarity([embeddings[0]], [embeddings[1]])[0][0]
|
| 44 |
+
cat_dog = cosine_similarity([embeddings[0]], [embeddings[2]])[0][0]
|
|
|
|
|
|
|
|
|
|
|
|
test_session.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Test session manager to verify context persistence across conversations."""
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
import sys
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
# Add parent directory to path
|
| 8 |
+
sys.path.insert(0, str(Path(__file__).parent))
|
| 9 |
+
|
| 10 |
+
from agent_framework import Agent, LlmClient, InMemorySessionManager, display_trace
|
| 11 |
+
from agent_tools import calculator
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
async def main():
|
| 15 |
+
"""Test session persistence."""
|
| 16 |
+
|
| 17 |
+
print("=" * 60)
|
| 18 |
+
print("Session Manager Test - Context Persistence")
|
| 19 |
+
print("=" * 60)
|
| 20 |
+
|
| 21 |
+
# Create a shared session manager
|
| 22 |
+
session_manager = InMemorySessionManager()
|
| 23 |
+
|
| 24 |
+
# Create agent with session support
|
| 25 |
+
agent = Agent(
|
| 26 |
+
model=LlmClient(model="gpt-4o-mini"),
|
| 27 |
+
tools=[calculator],
|
| 28 |
+
instructions="You are a helpful assistant with memory. Remember what users tell you.",
|
| 29 |
+
max_steps=5,
|
| 30 |
+
session_manager=session_manager
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
session_id = "test-user-123"
|
| 34 |
+
|
| 35 |
+
# === Conversation 1: Introduce yourself ===
|
| 36 |
+
print("\n" + "-" * 60)
|
| 37 |
+
print("Conversation 1: User introduces themselves")
|
| 38 |
+
print("-" * 60)
|
| 39 |
+
|
| 40 |
+
result1 = await agent.run(
|
| 41 |
+
"Hi! My name is Alice and I'm a software engineer. I love Python.",
|
| 42 |
+
session_id=session_id
|
| 43 |
+
)
|
| 44 |
+
print(f"User: Hi! My name is Alice and I'm a software engineer. I love Python.")
|
| 45 |
+
print(f"Agent: {result1.output}")
|
| 46 |
+
print(f"Events in context: {len(result1.context.events)}")
|
| 47 |
+
|
| 48 |
+
# === Conversation 2: Ask about something else ===
|
| 49 |
+
print("\n" + "-" * 60)
|
| 50 |
+
print("Conversation 2: Continue conversation")
|
| 51 |
+
print("-" * 60)
|
| 52 |
+
|
| 53 |
+
result2 = await agent.run(
|
| 54 |
+
"What's 1234 * 5678?",
|
| 55 |
+
session_id=session_id
|
| 56 |
+
)
|
| 57 |
+
print(f"User: What's 1234 * 5678?")
|
| 58 |
+
print(f"Agent: {result2.output}")
|
| 59 |
+
print(f"Events in context: {len(result2.context.events)}")
|
| 60 |
+
|
| 61 |
+
# === Conversation 3: Test if it remembers ===
|
| 62 |
+
print("\n" + "-" * 60)
|
| 63 |
+
print("Conversation 3: Test memory - Does it remember?")
|
| 64 |
+
print("-" * 60)
|
| 65 |
+
|
| 66 |
+
result3 = await agent.run(
|
| 67 |
+
"What's my name and what do I do for work?",
|
| 68 |
+
session_id=session_id
|
| 69 |
+
)
|
| 70 |
+
print(f"User: What's my name and what do I do for work?")
|
| 71 |
+
print(f"Agent: {result3.output}")
|
| 72 |
+
print(f"Events in context: {len(result3.context.events)}")
|
| 73 |
+
|
| 74 |
+
# === Test with a DIFFERENT session ===
|
| 75 |
+
print("\n" + "-" * 60)
|
| 76 |
+
print("Conversation 4: Different session (should NOT remember)")
|
| 77 |
+
print("-" * 60)
|
| 78 |
+
|
| 79 |
+
result4 = await agent.run(
|
| 80 |
+
"What's my name?",
|
| 81 |
+
session_id="different-user-456" # Different session!
|
| 82 |
+
)
|
| 83 |
+
print(f"User: What's my name?")
|
| 84 |
+
print(f"Agent: {result4.output}")
|
| 85 |
+
print(f"Events in context: {len(result4.context.events)}")
|
| 86 |
+
|
| 87 |
+
# === Show session storage ===
|
| 88 |
+
print("\n" + "=" * 60)
|
| 89 |
+
print("Session Storage Summary")
|
| 90 |
+
print("=" * 60)
|
| 91 |
+
|
| 92 |
+
# Access internal storage to show what's stored
|
| 93 |
+
for sid, session in session_manager._sessions.items():
|
| 94 |
+
print(f"\nSession ID: {sid}")
|
| 95 |
+
print(f" Events: {len(session.events)}")
|
| 96 |
+
print(f" State keys: {list(session.state.keys())}")
|
| 97 |
+
print(f" Created: {session.created_at}")
|
| 98 |
+
|
| 99 |
+
# === Optional: Show full trace ===
|
| 100 |
+
print("\n" + "=" * 60)
|
| 101 |
+
print("Full Trace for Session 'test-user-123' (Last Conversation)")
|
| 102 |
+
print("=" * 60)
|
| 103 |
+
display_trace(result3.context)
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
if __name__ == "__main__":
|
| 107 |
+
asyncio.run(main())
|
| 108 |
+
|
web_app/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Agent Chat Web Application
|
| 2 |
+
|
| 3 |
+
A modern chat interface for interacting with the AI agent framework.
|
| 4 |
+
|
| 5 |
+
## Features
|
| 6 |
+
|
| 7 |
+
- Real-time chat with AI agent
|
| 8 |
+
- Session memory toggle (on/off)
|
| 9 |
+
- File upload support
|
| 10 |
+
- Display of available tools
|
| 11 |
+
- Tool usage indicators in responses
|
| 12 |
+
|
| 13 |
+
## Running the Application
|
| 14 |
+
|
| 15 |
+
### Option 1: Direct run
|
| 16 |
+
|
| 17 |
+
```bash
|
| 18 |
+
cd web_app
|
| 19 |
+
python app.py
|
| 20 |
+
```
|
| 21 |
+
|
| 22 |
+
### Option 2: With uvicorn (recommended)
|
| 23 |
+
|
| 24 |
+
```bash
|
| 25 |
+
uvicorn web_app.app:app --reload --host 0.0.0.0 --port 8000
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
Then open http://localhost:8000 in your browser.
|
| 29 |
+
|
| 30 |
+
## API Endpoints
|
| 31 |
+
|
| 32 |
+
| Endpoint | Method | Description |
|
| 33 |
+
|----------|--------|-------------|
|
| 34 |
+
| `/` | GET | Chat interface |
|
| 35 |
+
| `/api/tools` | GET | List available tools |
|
| 36 |
+
| `/api/chat` | POST | Send message to agent |
|
| 37 |
+
| `/api/upload` | POST | Upload a file |
|
| 38 |
+
| `/api/uploads` | GET | List uploaded files |
|
| 39 |
+
| `/api/uploads/{filename}` | DELETE | Delete uploaded file |
|
| 40 |
+
| `/api/sessions` | GET | List active sessions |
|
| 41 |
+
| `/api/sessions/{session_id}` | DELETE | Clear a session |
|
| 42 |
+
|
| 43 |
+
## Chat Request Format
|
| 44 |
+
|
| 45 |
+
```json
|
| 46 |
+
{
|
| 47 |
+
"message": "Your message here",
|
| 48 |
+
"session_id": "optional-session-id",
|
| 49 |
+
"use_session": true
|
| 50 |
+
}
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
## Chat Response Format
|
| 54 |
+
|
| 55 |
+
```json
|
| 56 |
+
{
|
| 57 |
+
"response": "Agent's response",
|
| 58 |
+
"session_id": "session-uuid",
|
| 59 |
+
"events_count": 4,
|
| 60 |
+
"tools_used": ["calculator", "search_web"]
|
| 61 |
+
}
|
| 62 |
+
```
|
| 63 |
+
|
web_app/app.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""FastAPI web application for the Agent Framework."""
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import sys
|
| 5 |
+
import uuid
|
| 6 |
+
import shutil
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import Optional, List
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
|
| 11 |
+
# Add parent directory to path
|
| 12 |
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
| 13 |
+
|
| 14 |
+
from fastapi import FastAPI, UploadFile, File, Form, HTTPException
|
| 15 |
+
from fastapi.staticfiles import StaticFiles
|
| 16 |
+
from fastapi.responses import HTMLResponse, FileResponse
|
| 17 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 18 |
+
from pydantic import BaseModel
|
| 19 |
+
from dotenv import load_dotenv
|
| 20 |
+
|
| 21 |
+
from agent_framework import (
|
| 22 |
+
Agent, LlmClient, InMemorySessionManager,
|
| 23 |
+
display_trace, ExecutionContext, format_trace
|
| 24 |
+
)
|
| 25 |
+
from agent_tools import calculator, search_web, read_file, list_files, unzip_file, read_media_file
|
| 26 |
+
|
| 27 |
+
# Load environment variables
|
| 28 |
+
load_dotenv()
|
| 29 |
+
|
| 30 |
+
app = FastAPI(title="Agent Chat", description="AI Agent with Tools")
|
| 31 |
+
|
| 32 |
+
# Enable CORS
|
| 33 |
+
app.add_middleware(
|
| 34 |
+
CORSMiddleware,
|
| 35 |
+
allow_origins=["*"],
|
| 36 |
+
allow_credentials=True,
|
| 37 |
+
allow_methods=["*"],
|
| 38 |
+
allow_headers=["*"],
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
# Global session manager (shared across requests)
|
| 42 |
+
session_manager = InMemorySessionManager()
|
| 43 |
+
|
| 44 |
+
# Upload directory for files
|
| 45 |
+
UPLOAD_DIR = Path(__file__).parent / "uploads"
|
| 46 |
+
UPLOAD_DIR.mkdir(exist_ok=True)
|
| 47 |
+
|
| 48 |
+
# Available tools
|
| 49 |
+
TOOLS = [calculator, search_web, read_file, list_files, unzip_file, read_media_file]
|
| 50 |
+
|
| 51 |
+
# Create agent
|
| 52 |
+
def create_agent(use_session: bool = True) -> Agent:
|
| 53 |
+
"""Create an agent instance."""
|
| 54 |
+
|
| 55 |
+
# Include the actual upload directory path in instructions
|
| 56 |
+
upload_path = str(UPLOAD_DIR.absolute())
|
| 57 |
+
|
| 58 |
+
instructions = f"""You are a helpful AI assistant with access to various tools.
|
| 59 |
+
|
| 60 |
+
You can:
|
| 61 |
+
- Perform calculations using the calculator
|
| 62 |
+
- Search the web for current information
|
| 63 |
+
- Read excel files using the read_file tool
|
| 64 |
+
- List files in directories using the list_files tool
|
| 65 |
+
- Extract zip files using the unzip_file tool
|
| 66 |
+
- Read pdf using read_media_file
|
| 67 |
+
|
| 68 |
+
IMPORTANT - Uploaded files location:
|
| 69 |
+
Files uploaded by users are stored at: {upload_path}
|
| 70 |
+
To see uploaded files, use: list_files("{upload_path}")
|
| 71 |
+
To read a file, use: read_file("{upload_path}/filename.ext")
|
| 72 |
+
|
| 73 |
+
Always be helpful and use your tools when needed to provide accurate answers."""
|
| 74 |
+
|
| 75 |
+
return Agent(
|
| 76 |
+
model=LlmClient(model="gpt-4o-mini"),
|
| 77 |
+
tools=TOOLS,
|
| 78 |
+
instructions=instructions,
|
| 79 |
+
max_steps=10,
|
| 80 |
+
session_manager=session_manager if use_session else None
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
# Pydantic models for API
|
| 85 |
+
class ChatRequest(BaseModel):
|
| 86 |
+
message: str
|
| 87 |
+
session_id: Optional[str] = None
|
| 88 |
+
use_session: bool = True
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
class ChatResponse(BaseModel):
|
| 92 |
+
response: str
|
| 93 |
+
session_id: str
|
| 94 |
+
events_count: int
|
| 95 |
+
tools_used: List[str]
|
| 96 |
+
trace_text: str = "" # Simple text-based trace like display_trace
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
class ToolInfo(BaseModel):
|
| 100 |
+
name: str
|
| 101 |
+
description: str
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
class SessionInfo(BaseModel):
|
| 105 |
+
session_id: str
|
| 106 |
+
events_count: int
|
| 107 |
+
created_at: str
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
# API Endpoints
|
| 111 |
+
@app.get("/")
|
| 112 |
+
async def root():
|
| 113 |
+
"""Serve the chat interface."""
|
| 114 |
+
return FileResponse(Path(__file__).parent / "static" / "index.html")
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
@app.get("/api/tools")
|
| 118 |
+
async def get_tools() -> List[ToolInfo]:
|
| 119 |
+
"""Get list of available tools."""
|
| 120 |
+
return [
|
| 121 |
+
ToolInfo(
|
| 122 |
+
name=tool.name,
|
| 123 |
+
description=tool.description[:100] + "..." if len(tool.description) > 100 else tool.description
|
| 124 |
+
)
|
| 125 |
+
for tool in TOOLS
|
| 126 |
+
]
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
@app.post("/api/chat")
|
| 130 |
+
async def chat(request: ChatRequest) -> ChatResponse:
|
| 131 |
+
"""Send a message to the agent."""
|
| 132 |
+
|
| 133 |
+
# Generate or use provided session ID
|
| 134 |
+
session_id = request.session_id or str(uuid.uuid4())
|
| 135 |
+
|
| 136 |
+
# Create agent
|
| 137 |
+
agent = create_agent(use_session=request.use_session)
|
| 138 |
+
|
| 139 |
+
try:
|
| 140 |
+
# Run the agent
|
| 141 |
+
if request.use_session:
|
| 142 |
+
result = await agent.run(request.message, session_id=session_id)
|
| 143 |
+
else:
|
| 144 |
+
result = await agent.run(request.message)
|
| 145 |
+
|
| 146 |
+
# Extract tools used
|
| 147 |
+
tools_used = []
|
| 148 |
+
for event in result.context.events:
|
| 149 |
+
for item in event.content:
|
| 150 |
+
if hasattr(item, 'name') and item.type == "tool_call":
|
| 151 |
+
if item.name not in tools_used:
|
| 152 |
+
tools_used.append(item.name)
|
| 153 |
+
|
| 154 |
+
# Use your format_trace function directly!
|
| 155 |
+
trace_text = format_trace(result.context)
|
| 156 |
+
|
| 157 |
+
return ChatResponse(
|
| 158 |
+
response=str(result.output) if result.output else "I couldn't generate a response.",
|
| 159 |
+
session_id=session_id,
|
| 160 |
+
events_count=len(result.context.events),
|
| 161 |
+
tools_used=tools_used,
|
| 162 |
+
trace_text=trace_text
|
| 163 |
+
)
|
| 164 |
+
except Exception as e:
|
| 165 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
@app.post("/api/upload")
|
| 169 |
+
async def upload_file(file: UploadFile = File(...)):
|
| 170 |
+
"""Upload a file for the agent to access."""
|
| 171 |
+
|
| 172 |
+
# Save file to uploads directory
|
| 173 |
+
file_path = UPLOAD_DIR / file.filename
|
| 174 |
+
|
| 175 |
+
try:
|
| 176 |
+
with open(file_path, "wb") as buffer:
|
| 177 |
+
shutil.copyfileobj(file.file, buffer)
|
| 178 |
+
|
| 179 |
+
return {
|
| 180 |
+
"filename": file.filename,
|
| 181 |
+
"path": str(file_path),
|
| 182 |
+
"size": file_path.stat().st_size,
|
| 183 |
+
"message": f"File uploaded successfully. You can reference it at: {file_path}"
|
| 184 |
+
}
|
| 185 |
+
except Exception as e:
|
| 186 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
@app.get("/api/uploads")
|
| 190 |
+
async def list_uploads():
|
| 191 |
+
"""List uploaded files."""
|
| 192 |
+
files = []
|
| 193 |
+
for f in UPLOAD_DIR.iterdir():
|
| 194 |
+
if f.is_file() and not f.name.startswith('.'):
|
| 195 |
+
files.append({
|
| 196 |
+
"name": f.name,
|
| 197 |
+
"path": str(f),
|
| 198 |
+
"size": f.stat().st_size
|
| 199 |
+
})
|
| 200 |
+
return files
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
@app.delete("/api/uploads/{filename}")
|
| 204 |
+
async def delete_upload(filename: str):
|
| 205 |
+
"""Delete an uploaded file."""
|
| 206 |
+
file_path = UPLOAD_DIR / filename
|
| 207 |
+
if file_path.exists():
|
| 208 |
+
file_path.unlink()
|
| 209 |
+
return {"message": f"Deleted {filename}"}
|
| 210 |
+
raise HTTPException(status_code=404, detail="File not found")
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
@app.get("/api/sessions")
|
| 214 |
+
async def list_sessions() -> List[SessionInfo]:
|
| 215 |
+
"""List all active sessions."""
|
| 216 |
+
sessions = []
|
| 217 |
+
for sid, session in session_manager._sessions.items():
|
| 218 |
+
sessions.append(SessionInfo(
|
| 219 |
+
session_id=sid,
|
| 220 |
+
events_count=len(session.events),
|
| 221 |
+
created_at=session.created_at.isoformat()
|
| 222 |
+
))
|
| 223 |
+
return sessions
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
@app.delete("/api/sessions/{session_id}")
|
| 227 |
+
async def delete_session(session_id: str):
|
| 228 |
+
"""Delete a session to clear conversation history."""
|
| 229 |
+
if session_id in session_manager._sessions:
|
| 230 |
+
del session_manager._sessions[session_id]
|
| 231 |
+
return {"message": f"Session {session_id} cleared"}
|
| 232 |
+
raise HTTPException(status_code=404, detail="Session not found")
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
# Mount static files
|
| 236 |
+
static_dir = Path(__file__).parent / "static"
|
| 237 |
+
static_dir.mkdir(exist_ok=True)
|
| 238 |
+
app.mount("/static", StaticFiles(directory=static_dir), name="static")
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
if __name__ == "__main__":
|
| 242 |
+
import uvicorn
|
| 243 |
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
| 244 |
+
|
web_app/static/index.html
ADDED
|
@@ -0,0 +1,1012 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Agent Chat</title>
|
| 7 |
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
| 8 |
+
<style>
|
| 9 |
+
:root {
|
| 10 |
+
--bg-primary: #0a0a0f;
|
| 11 |
+
--bg-secondary: #12121a;
|
| 12 |
+
--bg-tertiary: #1a1a25;
|
| 13 |
+
--accent: #00d4aa;
|
| 14 |
+
--accent-dim: #00a080;
|
| 15 |
+
--text-primary: #e8e8e8;
|
| 16 |
+
--text-secondary: #888;
|
| 17 |
+
--border: #2a2a3a;
|
| 18 |
+
--user-msg: #1e3a5f;
|
| 19 |
+
--agent-msg: #1a2a1a;
|
| 20 |
+
--tool-tag: #2d1f4e;
|
| 21 |
+
--error: #ff4757;
|
| 22 |
+
--success: #00d4aa;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
html {
|
| 26 |
+
height: 100%;
|
| 27 |
+
width: 100%;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
* {
|
| 31 |
+
margin: 0;
|
| 32 |
+
padding: 0;
|
| 33 |
+
-webkit-box-sizing: border-box;
|
| 34 |
+
box-sizing: border-box;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
body {
|
| 38 |
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
| 39 |
+
background-color: #0a0a0f;
|
| 40 |
+
background: var(--bg-primary);
|
| 41 |
+
color: #e8e8e8;
|
| 42 |
+
color: var(--text-primary);
|
| 43 |
+
min-height: 100vh;
|
| 44 |
+
height: 100vh;
|
| 45 |
+
display: -webkit-box;
|
| 46 |
+
display: -webkit-flex;
|
| 47 |
+
display: flex;
|
| 48 |
+
overflow: hidden;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
/* Sidebar */
|
| 52 |
+
.sidebar {
|
| 53 |
+
width: 280px;
|
| 54 |
+
min-width: 280px;
|
| 55 |
+
background: var(--bg-secondary);
|
| 56 |
+
border-right: 1px solid var(--border);
|
| 57 |
+
display: -webkit-box;
|
| 58 |
+
display: -webkit-flex;
|
| 59 |
+
display: flex;
|
| 60 |
+
-webkit-flex-direction: column;
|
| 61 |
+
flex-direction: column;
|
| 62 |
+
padding: 20px;
|
| 63 |
+
overflow-y: auto;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
.logo {
|
| 67 |
+
font-family: 'JetBrains Mono', monospace;
|
| 68 |
+
font-size: 1.4rem;
|
| 69 |
+
font-weight: 600;
|
| 70 |
+
color: var(--accent);
|
| 71 |
+
margin-bottom: 30px;
|
| 72 |
+
display: flex;
|
| 73 |
+
align-items: center;
|
| 74 |
+
gap: 10px;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
.logo::before {
|
| 78 |
+
content: '>';
|
| 79 |
+
animation: blink 1s infinite;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
@keyframes blink {
|
| 83 |
+
50% { opacity: 0; }
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
.section-title {
|
| 87 |
+
font-size: 0.75rem;
|
| 88 |
+
font-weight: 600;
|
| 89 |
+
text-transform: uppercase;
|
| 90 |
+
letter-spacing: 1px;
|
| 91 |
+
color: var(--text-secondary);
|
| 92 |
+
margin-bottom: 12px;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
/* Session Toggle */
|
| 96 |
+
.session-control {
|
| 97 |
+
background: var(--bg-tertiary);
|
| 98 |
+
border-radius: 12px;
|
| 99 |
+
padding: 16px;
|
| 100 |
+
margin-bottom: 24px;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
.toggle-container {
|
| 104 |
+
display: flex;
|
| 105 |
+
align-items: center;
|
| 106 |
+
justify-content: space-between;
|
| 107 |
+
margin-top: 10px;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
.toggle-label {
|
| 111 |
+
font-size: 0.9rem;
|
| 112 |
+
color: var(--text-primary);
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
.toggle {
|
| 116 |
+
position: relative;
|
| 117 |
+
width: 50px;
|
| 118 |
+
height: 26px;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
.toggle input {
|
| 122 |
+
opacity: 0;
|
| 123 |
+
width: 0;
|
| 124 |
+
height: 0;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
.toggle-slider {
|
| 128 |
+
position: absolute;
|
| 129 |
+
cursor: pointer;
|
| 130 |
+
inset: 0;
|
| 131 |
+
background: var(--bg-primary);
|
| 132 |
+
border-radius: 26px;
|
| 133 |
+
transition: 0.3s;
|
| 134 |
+
border: 2px solid var(--border);
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
.toggle-slider::before {
|
| 138 |
+
position: absolute;
|
| 139 |
+
content: "";
|
| 140 |
+
height: 18px;
|
| 141 |
+
width: 18px;
|
| 142 |
+
left: 2px;
|
| 143 |
+
bottom: 2px;
|
| 144 |
+
background: var(--text-secondary);
|
| 145 |
+
border-radius: 50%;
|
| 146 |
+
transition: 0.3s;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
.toggle input:checked + .toggle-slider {
|
| 150 |
+
background: var(--accent-dim);
|
| 151 |
+
border-color: var(--accent);
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
.toggle input:checked + .toggle-slider::before {
|
| 155 |
+
transform: translateX(24px);
|
| 156 |
+
background: var(--accent);
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
.session-info {
|
| 160 |
+
font-size: 0.8rem;
|
| 161 |
+
color: var(--text-secondary);
|
| 162 |
+
margin-top: 10px;
|
| 163 |
+
font-family: 'JetBrains Mono', monospace;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
/* Tools List */
|
| 167 |
+
.tools-section {
|
| 168 |
+
flex: 1;
|
| 169 |
+
overflow-y: auto;
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
.tool-item {
|
| 173 |
+
background: var(--bg-tertiary);
|
| 174 |
+
border-radius: 8px;
|
| 175 |
+
padding: 12px;
|
| 176 |
+
margin-bottom: 8px;
|
| 177 |
+
border: 1px solid transparent;
|
| 178 |
+
transition: all 0.2s;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
.tool-item:hover {
|
| 182 |
+
border-color: var(--accent-dim);
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
.tool-name {
|
| 186 |
+
font-family: 'JetBrains Mono', monospace;
|
| 187 |
+
font-size: 0.85rem;
|
| 188 |
+
color: var(--accent);
|
| 189 |
+
margin-bottom: 4px;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
.tool-desc {
|
| 193 |
+
font-size: 0.75rem;
|
| 194 |
+
color: var(--text-secondary);
|
| 195 |
+
line-height: 1.4;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
/* Files Section */
|
| 199 |
+
.files-section {
|
| 200 |
+
margin-top: 20px;
|
| 201 |
+
padding-top: 20px;
|
| 202 |
+
border-top: 1px solid var(--border);
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
.file-item {
|
| 206 |
+
display: flex;
|
| 207 |
+
align-items: center;
|
| 208 |
+
justify-content: space-between;
|
| 209 |
+
background: var(--bg-tertiary);
|
| 210 |
+
border-radius: 8px;
|
| 211 |
+
padding: 10px 12px;
|
| 212 |
+
margin-bottom: 8px;
|
| 213 |
+
font-size: 0.8rem;
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
.file-name {
|
| 217 |
+
font-family: 'JetBrains Mono', monospace;
|
| 218 |
+
color: var(--text-primary);
|
| 219 |
+
overflow: hidden;
|
| 220 |
+
text-overflow: ellipsis;
|
| 221 |
+
white-space: nowrap;
|
| 222 |
+
flex: 1;
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
.file-delete {
|
| 226 |
+
background: none;
|
| 227 |
+
border: none;
|
| 228 |
+
color: var(--error);
|
| 229 |
+
cursor: pointer;
|
| 230 |
+
padding: 4px;
|
| 231 |
+
opacity: 0.6;
|
| 232 |
+
transition: opacity 0.2s;
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
.file-delete:hover {
|
| 236 |
+
opacity: 1;
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
/* Main Chat Area */
|
| 240 |
+
.main {
|
| 241 |
+
-webkit-box-flex: 1;
|
| 242 |
+
-webkit-flex: 1;
|
| 243 |
+
flex: 1;
|
| 244 |
+
display: -webkit-box;
|
| 245 |
+
display: -webkit-flex;
|
| 246 |
+
display: flex;
|
| 247 |
+
-webkit-flex-direction: column;
|
| 248 |
+
flex-direction: column;
|
| 249 |
+
height: 100vh;
|
| 250 |
+
max-height: 100vh;
|
| 251 |
+
overflow: hidden;
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
.chat-header {
|
| 255 |
+
padding: 20px 30px;
|
| 256 |
+
border-bottom: 1px solid var(--border);
|
| 257 |
+
display: flex;
|
| 258 |
+
align-items: center;
|
| 259 |
+
justify-content: space-between;
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
.chat-title {
|
| 263 |
+
font-size: 1.1rem;
|
| 264 |
+
font-weight: 600;
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
.header-buttons {
|
| 268 |
+
display: flex;
|
| 269 |
+
gap: 10px;
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
.header-btn {
|
| 273 |
+
background: var(--bg-tertiary);
|
| 274 |
+
border: 1px solid var(--border);
|
| 275 |
+
color: var(--text-secondary);
|
| 276 |
+
padding: 8px 16px;
|
| 277 |
+
border-radius: 8px;
|
| 278 |
+
cursor: pointer;
|
| 279 |
+
font-size: 0.85rem;
|
| 280 |
+
transition: all 0.2s;
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
.header-btn:hover {
|
| 284 |
+
border-color: var(--accent);
|
| 285 |
+
color: var(--accent);
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
.clear-btn:hover {
|
| 289 |
+
border-color: var(--error) !important;
|
| 290 |
+
color: var(--error) !important;
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
/* Trace Modal */
|
| 294 |
+
.modal-overlay {
|
| 295 |
+
display: none;
|
| 296 |
+
position: fixed;
|
| 297 |
+
inset: 0;
|
| 298 |
+
background: rgba(0, 0, 0, 0.8);
|
| 299 |
+
z-index: 1000;
|
| 300 |
+
-webkit-backdrop-filter: blur(4px);
|
| 301 |
+
backdrop-filter: blur(4px);
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
.modal-overlay.active {
|
| 305 |
+
display: flex;
|
| 306 |
+
align-items: center;
|
| 307 |
+
justify-content: center;
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
.modal {
|
| 311 |
+
background: var(--bg-secondary);
|
| 312 |
+
border: 1px solid var(--border);
|
| 313 |
+
border-radius: 16px;
|
| 314 |
+
width: 90%;
|
| 315 |
+
max-width: 800px;
|
| 316 |
+
max-height: 80vh;
|
| 317 |
+
display: flex;
|
| 318 |
+
flex-direction: column;
|
| 319 |
+
animation: slideIn 0.3s ease;
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
@keyframes slideIn {
|
| 323 |
+
from { opacity: 0; transform: translateY(-20px); }
|
| 324 |
+
to { opacity: 1; transform: translateY(0); }
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
.modal-header {
|
| 328 |
+
padding: 20px;
|
| 329 |
+
border-bottom: 1px solid var(--border);
|
| 330 |
+
display: flex;
|
| 331 |
+
justify-content: space-between;
|
| 332 |
+
align-items: center;
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
.modal-title {
|
| 336 |
+
font-size: 1.2rem;
|
| 337 |
+
font-weight: 600;
|
| 338 |
+
color: var(--accent);
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
.modal-close {
|
| 342 |
+
background: none;
|
| 343 |
+
border: none;
|
| 344 |
+
color: var(--text-secondary);
|
| 345 |
+
cursor: pointer;
|
| 346 |
+
padding: 8px;
|
| 347 |
+
font-size: 1.5rem;
|
| 348 |
+
line-height: 1;
|
| 349 |
+
}
|
| 350 |
+
|
| 351 |
+
.modal-close:hover {
|
| 352 |
+
color: var(--text-primary);
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
.modal-body {
|
| 356 |
+
padding: 20px;
|
| 357 |
+
overflow-y: auto;
|
| 358 |
+
-webkit-overflow-scrolling: touch;
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
.trace-step {
|
| 362 |
+
background: var(--bg-tertiary);
|
| 363 |
+
border-radius: 8px;
|
| 364 |
+
padding: 16px;
|
| 365 |
+
margin-bottom: 12px;
|
| 366 |
+
border-left: 3px solid var(--accent);
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
.trace-step-header {
|
| 370 |
+
display: flex;
|
| 371 |
+
justify-content: space-between;
|
| 372 |
+
align-items: center;
|
| 373 |
+
margin-bottom: 10px;
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
.trace-step-num {
|
| 377 |
+
font-family: 'JetBrains Mono', monospace;
|
| 378 |
+
font-size: 0.8rem;
|
| 379 |
+
color: var(--accent);
|
| 380 |
+
background: var(--bg-primary);
|
| 381 |
+
padding: 4px 8px;
|
| 382 |
+
border-radius: 4px;
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
.trace-author {
|
| 386 |
+
font-size: 0.85rem;
|
| 387 |
+
color: var(--text-secondary);
|
| 388 |
+
text-transform: uppercase;
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
.trace-item {
|
| 392 |
+
margin-top: 8px;
|
| 393 |
+
padding: 10px;
|
| 394 |
+
background: var(--bg-primary);
|
| 395 |
+
border-radius: 6px;
|
| 396 |
+
font-size: 0.85rem;
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
.trace-item-type {
|
| 400 |
+
font-family: 'JetBrains Mono', monospace;
|
| 401 |
+
font-size: 0.75rem;
|
| 402 |
+
padding: 2px 6px;
|
| 403 |
+
border-radius: 3px;
|
| 404 |
+
margin-bottom: 6px;
|
| 405 |
+
display: inline-block;
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
.trace-item-type.message { background: var(--user-msg); color: #7cb3d4; }
|
| 409 |
+
.trace-item-type.tool_call { background: var(--tool-tag); color: var(--accent); }
|
| 410 |
+
.trace-item-type.tool_result { background: var(--agent-msg); color: #7cd47c; }
|
| 411 |
+
|
| 412 |
+
.trace-content {
|
| 413 |
+
color: var(--text-primary);
|
| 414 |
+
line-height: 1.5;
|
| 415 |
+
white-space: pre-wrap;
|
| 416 |
+
word-break: break-word;
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
.trace-args {
|
| 420 |
+
font-family: 'JetBrains Mono', monospace;
|
| 421 |
+
font-size: 0.8rem;
|
| 422 |
+
color: var(--text-secondary);
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
/* Messages */
|
| 426 |
+
.messages {
|
| 427 |
+
-webkit-box-flex: 1;
|
| 428 |
+
-webkit-flex: 1;
|
| 429 |
+
flex: 1;
|
| 430 |
+
overflow-y: auto;
|
| 431 |
+
-webkit-overflow-scrolling: touch;
|
| 432 |
+
padding: 30px;
|
| 433 |
+
display: -webkit-box;
|
| 434 |
+
display: -webkit-flex;
|
| 435 |
+
display: flex;
|
| 436 |
+
-webkit-flex-direction: column;
|
| 437 |
+
flex-direction: column;
|
| 438 |
+
gap: 20px;
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
+
.message {
|
| 442 |
+
max-width: 80%;
|
| 443 |
+
padding: 16px 20px;
|
| 444 |
+
border-radius: 16px;
|
| 445 |
+
line-height: 1.6;
|
| 446 |
+
animation: fadeIn 0.3s ease;
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
@keyframes fadeIn {
|
| 450 |
+
from { opacity: 0; transform: translateY(10px); }
|
| 451 |
+
to { opacity: 1; transform: translateY(0); }
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
.message.user {
|
| 455 |
+
background: var(--user-msg);
|
| 456 |
+
align-self: flex-end;
|
| 457 |
+
border-bottom-right-radius: 4px;
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
.message.agent {
|
| 461 |
+
background: var(--bg-tertiary);
|
| 462 |
+
align-self: flex-start;
|
| 463 |
+
border-bottom-left-radius: 4px;
|
| 464 |
+
border: 1px solid var(--border);
|
| 465 |
+
}
|
| 466 |
+
|
| 467 |
+
.message-meta {
|
| 468 |
+
display: flex;
|
| 469 |
+
align-items: center;
|
| 470 |
+
gap: 10px;
|
| 471 |
+
margin-top: 10px;
|
| 472 |
+
font-size: 0.75rem;
|
| 473 |
+
color: var(--text-secondary);
|
| 474 |
+
}
|
| 475 |
+
|
| 476 |
+
.tool-tag {
|
| 477 |
+
background: var(--tool-tag);
|
| 478 |
+
color: var(--accent);
|
| 479 |
+
padding: 2px 8px;
|
| 480 |
+
border-radius: 4px;
|
| 481 |
+
font-family: 'JetBrains Mono', monospace;
|
| 482 |
+
font-size: 0.7rem;
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
+
/* Input Area */
|
| 486 |
+
.input-area {
|
| 487 |
+
padding: 20px 30px;
|
| 488 |
+
border-top: 1px solid var(--border);
|
| 489 |
+
background: var(--bg-secondary);
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
.input-container {
|
| 493 |
+
display: flex;
|
| 494 |
+
gap: 12px;
|
| 495 |
+
align-items: flex-end;
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
.input-wrapper {
|
| 499 |
+
flex: 1;
|
| 500 |
+
position: relative;
|
| 501 |
+
}
|
| 502 |
+
|
| 503 |
+
textarea {
|
| 504 |
+
width: 100%;
|
| 505 |
+
background: var(--bg-primary);
|
| 506 |
+
border: 1px solid var(--border);
|
| 507 |
+
border-radius: 12px;
|
| 508 |
+
padding: 16px 20px;
|
| 509 |
+
color: var(--text-primary);
|
| 510 |
+
font-family: inherit;
|
| 511 |
+
font-size: 0.95rem;
|
| 512 |
+
resize: none;
|
| 513 |
+
min-height: 56px;
|
| 514 |
+
max-height: 200px;
|
| 515 |
+
outline: none;
|
| 516 |
+
transition: border-color 0.2s;
|
| 517 |
+
}
|
| 518 |
+
|
| 519 |
+
textarea:focus {
|
| 520 |
+
border-color: var(--accent);
|
| 521 |
+
}
|
| 522 |
+
|
| 523 |
+
textarea::placeholder {
|
| 524 |
+
color: var(--text-secondary);
|
| 525 |
+
}
|
| 526 |
+
|
| 527 |
+
.btn-group {
|
| 528 |
+
display: flex;
|
| 529 |
+
gap: 8px;
|
| 530 |
+
}
|
| 531 |
+
|
| 532 |
+
.btn {
|
| 533 |
+
background: var(--accent);
|
| 534 |
+
border: none;
|
| 535 |
+
color: var(--bg-primary);
|
| 536 |
+
padding: 16px 24px;
|
| 537 |
+
border-radius: 12px;
|
| 538 |
+
cursor: pointer;
|
| 539 |
+
font-weight: 600;
|
| 540 |
+
font-size: 0.9rem;
|
| 541 |
+
transition: all 0.2s;
|
| 542 |
+
display: flex;
|
| 543 |
+
align-items: center;
|
| 544 |
+
gap: 8px;
|
| 545 |
+
}
|
| 546 |
+
|
| 547 |
+
.btn:hover {
|
| 548 |
+
background: var(--accent-dim);
|
| 549 |
+
transform: translateY(-1px);
|
| 550 |
+
}
|
| 551 |
+
|
| 552 |
+
.btn:disabled {
|
| 553 |
+
opacity: 0.5;
|
| 554 |
+
cursor: not-allowed;
|
| 555 |
+
transform: none;
|
| 556 |
+
}
|
| 557 |
+
|
| 558 |
+
.btn-upload {
|
| 559 |
+
background: var(--bg-tertiary);
|
| 560 |
+
border: 1px solid var(--border);
|
| 561 |
+
color: var(--text-primary);
|
| 562 |
+
}
|
| 563 |
+
|
| 564 |
+
.btn-upload:hover {
|
| 565 |
+
border-color: var(--accent);
|
| 566 |
+
color: var(--accent);
|
| 567 |
+
background: var(--bg-tertiary);
|
| 568 |
+
}
|
| 569 |
+
|
| 570 |
+
#file-input {
|
| 571 |
+
display: none;
|
| 572 |
+
}
|
| 573 |
+
|
| 574 |
+
/* Loading */
|
| 575 |
+
.loading {
|
| 576 |
+
display: flex;
|
| 577 |
+
gap: 6px;
|
| 578 |
+
padding: 20px;
|
| 579 |
+
}
|
| 580 |
+
|
| 581 |
+
.loading-dot {
|
| 582 |
+
width: 8px;
|
| 583 |
+
height: 8px;
|
| 584 |
+
background: var(--accent);
|
| 585 |
+
border-radius: 50%;
|
| 586 |
+
animation: bounce 1.4s infinite ease-in-out both;
|
| 587 |
+
}
|
| 588 |
+
|
| 589 |
+
.loading-dot:nth-child(1) { animation-delay: -0.32s; }
|
| 590 |
+
.loading-dot:nth-child(2) { animation-delay: -0.16s; }
|
| 591 |
+
|
| 592 |
+
@keyframes bounce {
|
| 593 |
+
0%, 80%, 100% { transform: scale(0); }
|
| 594 |
+
40% { transform: scale(1); }
|
| 595 |
+
}
|
| 596 |
+
|
| 597 |
+
/* Scrollbar */
|
| 598 |
+
::-webkit-scrollbar {
|
| 599 |
+
width: 8px;
|
| 600 |
+
}
|
| 601 |
+
|
| 602 |
+
::-webkit-scrollbar-track {
|
| 603 |
+
background: var(--bg-primary);
|
| 604 |
+
}
|
| 605 |
+
|
| 606 |
+
::-webkit-scrollbar-thumb {
|
| 607 |
+
background: var(--border);
|
| 608 |
+
border-radius: 4px;
|
| 609 |
+
}
|
| 610 |
+
|
| 611 |
+
::-webkit-scrollbar-thumb:hover {
|
| 612 |
+
background: var(--text-secondary);
|
| 613 |
+
}
|
| 614 |
+
|
| 615 |
+
/* Mobile responsive */
|
| 616 |
+
@media (max-width: 768px) {
|
| 617 |
+
.sidebar {
|
| 618 |
+
display: none;
|
| 619 |
+
}
|
| 620 |
+
}
|
| 621 |
+
</style>
|
| 622 |
+
</head>
|
| 623 |
+
<body>
|
| 624 |
+
<noscript>
|
| 625 |
+
<div style="padding: 50px; text-align: center; color: #00d4aa; font-size: 18px;">
|
| 626 |
+
Please enable JavaScript to use Agent Chat.
|
| 627 |
+
</div>
|
| 628 |
+
</noscript>
|
| 629 |
+
<aside class="sidebar">
|
| 630 |
+
<div class="logo">Agent Chat</div>
|
| 631 |
+
|
| 632 |
+
<div class="session-control">
|
| 633 |
+
<div class="section-title">Session Memory</div>
|
| 634 |
+
<div class="toggle-container">
|
| 635 |
+
<span class="toggle-label">Remember context</span>
|
| 636 |
+
<label class="toggle">
|
| 637 |
+
<input type="checkbox" id="session-toggle" checked>
|
| 638 |
+
<span class="toggle-slider"></span>
|
| 639 |
+
</label>
|
| 640 |
+
</div>
|
| 641 |
+
<div class="session-info" id="session-info">
|
| 642 |
+
Session: <span id="session-id">-</span>
|
| 643 |
+
</div>
|
| 644 |
+
</div>
|
| 645 |
+
|
| 646 |
+
<div class="tools-section">
|
| 647 |
+
<div class="section-title">Available Tools</div>
|
| 648 |
+
<div id="tools-list"></div>
|
| 649 |
+
</div>
|
| 650 |
+
|
| 651 |
+
<div class="files-section">
|
| 652 |
+
<div class="section-title">Uploaded Files</div>
|
| 653 |
+
<div id="files-list"></div>
|
| 654 |
+
</div>
|
| 655 |
+
</aside>
|
| 656 |
+
|
| 657 |
+
<main class="main">
|
| 658 |
+
<header class="chat-header">
|
| 659 |
+
<h1 class="chat-title">Chat with AI Agent</h1>
|
| 660 |
+
<div class="header-buttons">
|
| 661 |
+
<button class="header-btn" id="trace-btn">View Trace</button>
|
| 662 |
+
<button class="header-btn clear-btn" id="clear-btn">Clear Session</button>
|
| 663 |
+
</div>
|
| 664 |
+
</header>
|
| 665 |
+
|
| 666 |
+
<div class="messages" id="messages">
|
| 667 |
+
<div class="message agent">
|
| 668 |
+
Hello! I'm an AI assistant with access to various tools. I can help you with calculations, web searches, reading files, and more. How can I help you today?
|
| 669 |
+
</div>
|
| 670 |
+
</div>
|
| 671 |
+
|
| 672 |
+
<div class="input-area">
|
| 673 |
+
<div class="input-container">
|
| 674 |
+
<div class="input-wrapper">
|
| 675 |
+
<textarea
|
| 676 |
+
id="message-input"
|
| 677 |
+
placeholder="Type your message... (Enter to send, Shift+Enter for new line)"
|
| 678 |
+
rows="1"
|
| 679 |
+
></textarea>
|
| 680 |
+
</div>
|
| 681 |
+
<div class="btn-group">
|
| 682 |
+
<label class="btn btn-upload" for="file-input">
|
| 683 |
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 684 |
+
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12"/>
|
| 685 |
+
</svg>
|
| 686 |
+
</label>
|
| 687 |
+
<input type="file" id="file-input" multiple>
|
| 688 |
+
<button class="btn" id="send-btn">
|
| 689 |
+
Send
|
| 690 |
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 691 |
+
<path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z"/>
|
| 692 |
+
</svg>
|
| 693 |
+
</button>
|
| 694 |
+
</div>
|
| 695 |
+
</div>
|
| 696 |
+
</div>
|
| 697 |
+
</main>
|
| 698 |
+
|
| 699 |
+
<!-- Trace Modal -->
|
| 700 |
+
<div class="modal-overlay" id="trace-modal">
|
| 701 |
+
<div class="modal">
|
| 702 |
+
<div class="modal-header">
|
| 703 |
+
<h2 class="modal-title">Execution Trace</h2>
|
| 704 |
+
<button class="modal-close" id="modal-close">×</button>
|
| 705 |
+
</div>
|
| 706 |
+
<div class="modal-body" id="trace-content">
|
| 707 |
+
<p style="color: var(--text-secondary);">No trace available. Send a message first.</p>
|
| 708 |
+
</div>
|
| 709 |
+
</div>
|
| 710 |
+
</div>
|
| 711 |
+
|
| 712 |
+
<script>
|
| 713 |
+
// State
|
| 714 |
+
let sessionId = generateUUID();
|
| 715 |
+
let useSession = true;
|
| 716 |
+
let currentTrace = ""; // Text-based trace
|
| 717 |
+
|
| 718 |
+
function generateUUID() {
|
| 719 |
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
| 720 |
+
const r = Math.random() * 16 | 0;
|
| 721 |
+
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
| 722 |
+
return v.toString(16);
|
| 723 |
+
});
|
| 724 |
+
}
|
| 725 |
+
|
| 726 |
+
// DOM Elements
|
| 727 |
+
const messagesContainer = document.getElementById('messages');
|
| 728 |
+
const messageInput = document.getElementById('message-input');
|
| 729 |
+
const sendBtn = document.getElementById('send-btn');
|
| 730 |
+
const clearBtn = document.getElementById('clear-btn');
|
| 731 |
+
const sessionToggle = document.getElementById('session-toggle');
|
| 732 |
+
const sessionIdSpan = document.getElementById('session-id');
|
| 733 |
+
const toolsList = document.getElementById('tools-list');
|
| 734 |
+
const filesList = document.getElementById('files-list');
|
| 735 |
+
const fileInput = document.getElementById('file-input');
|
| 736 |
+
const traceBtn = document.getElementById('trace-btn');
|
| 737 |
+
const traceModal = document.getElementById('trace-modal');
|
| 738 |
+
const modalClose = document.getElementById('modal-close');
|
| 739 |
+
const traceContent = document.getElementById('trace-content');
|
| 740 |
+
|
| 741 |
+
// Initialize
|
| 742 |
+
async function init() {
|
| 743 |
+
await loadTools();
|
| 744 |
+
await loadFiles();
|
| 745 |
+
updateSessionDisplay();
|
| 746 |
+
}
|
| 747 |
+
|
| 748 |
+
function updateSessionDisplay() {
|
| 749 |
+
sessionIdSpan.textContent = useSession ? sessionId.substring(0, 8) + '...' : 'disabled';
|
| 750 |
+
}
|
| 751 |
+
|
| 752 |
+
// Load tools
|
| 753 |
+
async function loadTools() {
|
| 754 |
+
try {
|
| 755 |
+
const response = await fetch('/api/tools');
|
| 756 |
+
const tools = await response.json();
|
| 757 |
+
toolsList.innerHTML = tools.map(tool => `
|
| 758 |
+
<div class="tool-item">
|
| 759 |
+
<div class="tool-name">${tool.name}</div>
|
| 760 |
+
<div class="tool-desc">${tool.description}</div>
|
| 761 |
+
</div>
|
| 762 |
+
`).join('');
|
| 763 |
+
} catch (e) {
|
| 764 |
+
toolsList.innerHTML = '<div class="tool-item">Failed to load tools</div>';
|
| 765 |
+
}
|
| 766 |
+
}
|
| 767 |
+
|
| 768 |
+
// Load files
|
| 769 |
+
async function loadFiles() {
|
| 770 |
+
try {
|
| 771 |
+
const response = await fetch('/api/uploads');
|
| 772 |
+
const files = await response.json();
|
| 773 |
+
if (files.length === 0) {
|
| 774 |
+
filesList.innerHTML = '<div style="color: var(--text-secondary); font-size: 0.8rem;">No files uploaded</div>';
|
| 775 |
+
} else {
|
| 776 |
+
filesList.innerHTML = files.map(file => `
|
| 777 |
+
<div class="file-item">
|
| 778 |
+
<span class="file-name">${file.name}</span>
|
| 779 |
+
<button class="file-delete" onclick="deleteFile('${file.name}')">
|
| 780 |
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 781 |
+
<path d="M18 6L6 18M6 6l12 12"/>
|
| 782 |
+
</svg>
|
| 783 |
+
</button>
|
| 784 |
+
</div>
|
| 785 |
+
`).join('');
|
| 786 |
+
}
|
| 787 |
+
} catch (e) {
|
| 788 |
+
filesList.innerHTML = '<div style="color: var(--text-secondary); font-size: 0.8rem;">Failed to load files</div>';
|
| 789 |
+
}
|
| 790 |
+
}
|
| 791 |
+
|
| 792 |
+
// Delete file
|
| 793 |
+
async function deleteFile(filename) {
|
| 794 |
+
try {
|
| 795 |
+
await fetch(`/api/uploads/${encodeURIComponent(filename)}`, { method: 'DELETE' });
|
| 796 |
+
await loadFiles();
|
| 797 |
+
} catch (e) {
|
| 798 |
+
console.error('Failed to delete file:', e);
|
| 799 |
+
}
|
| 800 |
+
}
|
| 801 |
+
|
| 802 |
+
// Add message to chat
|
| 803 |
+
function addMessage(content, isUser, toolsUsed = []) {
|
| 804 |
+
const messageDiv = document.createElement('div');
|
| 805 |
+
messageDiv.className = `message ${isUser ? 'user' : 'agent'}`;
|
| 806 |
+
|
| 807 |
+
let html = content.replace(/\n/g, '<br>');
|
| 808 |
+
|
| 809 |
+
if (!isUser && toolsUsed.length > 0) {
|
| 810 |
+
const toolTags = toolsUsed.map(t => `<span class="tool-tag">${t}</span>`).join(' ');
|
| 811 |
+
html += `<div class="message-meta">Tools used: ${toolTags}</div>`;
|
| 812 |
+
}
|
| 813 |
+
|
| 814 |
+
messageDiv.innerHTML = html;
|
| 815 |
+
messagesContainer.appendChild(messageDiv);
|
| 816 |
+
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
| 817 |
+
}
|
| 818 |
+
|
| 819 |
+
// Show loading indicator
|
| 820 |
+
function showLoading() {
|
| 821 |
+
const loadingDiv = document.createElement('div');
|
| 822 |
+
loadingDiv.className = 'loading';
|
| 823 |
+
loadingDiv.id = 'loading';
|
| 824 |
+
loadingDiv.innerHTML = `
|
| 825 |
+
<div class="loading-dot"></div>
|
| 826 |
+
<div class="loading-dot"></div>
|
| 827 |
+
<div class="loading-dot"></div>
|
| 828 |
+
`;
|
| 829 |
+
messagesContainer.appendChild(loadingDiv);
|
| 830 |
+
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
| 831 |
+
}
|
| 832 |
+
|
| 833 |
+
function hideLoading() {
|
| 834 |
+
const loading = document.getElementById('loading');
|
| 835 |
+
if (loading) loading.remove();
|
| 836 |
+
}
|
| 837 |
+
|
| 838 |
+
// Send message
|
| 839 |
+
async function sendMessage() {
|
| 840 |
+
const message = messageInput.value.trim();
|
| 841 |
+
if (!message) return;
|
| 842 |
+
|
| 843 |
+
addMessage(message, true);
|
| 844 |
+
messageInput.value = '';
|
| 845 |
+
messageInput.style.height = 'auto';
|
| 846 |
+
sendBtn.disabled = true;
|
| 847 |
+
showLoading();
|
| 848 |
+
|
| 849 |
+
try {
|
| 850 |
+
const response = await fetch('/api/chat', {
|
| 851 |
+
method: 'POST',
|
| 852 |
+
headers: { 'Content-Type': 'application/json' },
|
| 853 |
+
body: JSON.stringify({
|
| 854 |
+
message: message,
|
| 855 |
+
session_id: useSession ? sessionId : null,
|
| 856 |
+
use_session: useSession
|
| 857 |
+
})
|
| 858 |
+
});
|
| 859 |
+
|
| 860 |
+
const data = await response.json();
|
| 861 |
+
hideLoading();
|
| 862 |
+
|
| 863 |
+
if (response.ok) {
|
| 864 |
+
addMessage(data.response, false, data.tools_used);
|
| 865 |
+
if (useSession) {
|
| 866 |
+
sessionId = data.session_id;
|
| 867 |
+
updateSessionDisplay();
|
| 868 |
+
}
|
| 869 |
+
// Store trace text for viewing
|
| 870 |
+
if (data.trace_text) {
|
| 871 |
+
currentTrace = data.trace_text;
|
| 872 |
+
}
|
| 873 |
+
} else {
|
| 874 |
+
addMessage(`Error: ${data.detail || 'Something went wrong'}`, false);
|
| 875 |
+
}
|
| 876 |
+
} catch (e) {
|
| 877 |
+
hideLoading();
|
| 878 |
+
addMessage(`Error: ${e.message}`, false);
|
| 879 |
+
}
|
| 880 |
+
|
| 881 |
+
sendBtn.disabled = false;
|
| 882 |
+
}
|
| 883 |
+
|
| 884 |
+
// Upload file
|
| 885 |
+
async function uploadFile(file) {
|
| 886 |
+
const formData = new FormData();
|
| 887 |
+
formData.append('file', file);
|
| 888 |
+
|
| 889 |
+
try {
|
| 890 |
+
const response = await fetch('/api/upload', {
|
| 891 |
+
method: 'POST',
|
| 892 |
+
body: formData
|
| 893 |
+
});
|
| 894 |
+
|
| 895 |
+
const data = await response.json();
|
| 896 |
+
if (response.ok) {
|
| 897 |
+
addMessage(
|
| 898 |
+
`File uploaded successfully: ${file.name}\n\n` +
|
| 899 |
+
`You can now ask me to:\n` +
|
| 900 |
+
`• "Read the file ${file.name}"\n` +
|
| 901 |
+
`• "What's in ${file.name}?"\n` +
|
| 902 |
+
`• "List my uploaded files"`,
|
| 903 |
+
false
|
| 904 |
+
);
|
| 905 |
+
await loadFiles();
|
| 906 |
+
} else {
|
| 907 |
+
addMessage(`Failed to upload ${file.name}: ${data.detail}`, false);
|
| 908 |
+
}
|
| 909 |
+
} catch (e) {
|
| 910 |
+
addMessage(`Failed to upload ${file.name}: ${e.message}`, false);
|
| 911 |
+
}
|
| 912 |
+
}
|
| 913 |
+
|
| 914 |
+
// Clear session
|
| 915 |
+
async function clearSession() {
|
| 916 |
+
if (useSession && sessionId) {
|
| 917 |
+
try {
|
| 918 |
+
await fetch(`/api/sessions/${sessionId}`, { method: 'DELETE' });
|
| 919 |
+
} catch (e) {
|
| 920 |
+
console.error('Failed to clear session:', e);
|
| 921 |
+
}
|
| 922 |
+
}
|
| 923 |
+
|
| 924 |
+
sessionId = generateUUID();
|
| 925 |
+
updateSessionDisplay();
|
| 926 |
+
messagesContainer.innerHTML = `
|
| 927 |
+
<div class="message agent">
|
| 928 |
+
Session cleared! I'm ready for a fresh conversation. How can I help you?
|
| 929 |
+
</div>
|
| 930 |
+
`;
|
| 931 |
+
}
|
| 932 |
+
|
| 933 |
+
// Event listeners
|
| 934 |
+
sendBtn.addEventListener('click', sendMessage);
|
| 935 |
+
|
| 936 |
+
messageInput.addEventListener('keydown', (e) => {
|
| 937 |
+
if (e.key === 'Enter' && !e.shiftKey) {
|
| 938 |
+
e.preventDefault();
|
| 939 |
+
sendMessage();
|
| 940 |
+
}
|
| 941 |
+
});
|
| 942 |
+
|
| 943 |
+
messageInput.addEventListener('input', () => {
|
| 944 |
+
messageInput.style.height = 'auto';
|
| 945 |
+
messageInput.style.height = Math.min(messageInput.scrollHeight, 200) + 'px';
|
| 946 |
+
});
|
| 947 |
+
|
| 948 |
+
sessionToggle.addEventListener('change', () => {
|
| 949 |
+
useSession = sessionToggle.checked;
|
| 950 |
+
updateSessionDisplay();
|
| 951 |
+
});
|
| 952 |
+
|
| 953 |
+
clearBtn.addEventListener('click', clearSession);
|
| 954 |
+
|
| 955 |
+
fileInput.addEventListener('change', async (e) => {
|
| 956 |
+
for (const file of e.target.files) {
|
| 957 |
+
await uploadFile(file);
|
| 958 |
+
}
|
| 959 |
+
fileInput.value = '';
|
| 960 |
+
});
|
| 961 |
+
|
| 962 |
+
// Trace modal functions
|
| 963 |
+
function renderTrace() {
|
| 964 |
+
if (!currentTrace) {
|
| 965 |
+
traceContent.innerHTML = '<p style="color: var(--text-secondary);">No trace available. Send a message first.</p>';
|
| 966 |
+
return;
|
| 967 |
+
}
|
| 968 |
+
|
| 969 |
+
// Display text trace in a pre-formatted block
|
| 970 |
+
traceContent.innerHTML = `<pre style="
|
| 971 |
+
font-family: 'JetBrains Mono', monospace;
|
| 972 |
+
font-size: 0.85rem;
|
| 973 |
+
line-height: 1.6;
|
| 974 |
+
white-space: pre-wrap;
|
| 975 |
+
word-break: break-word;
|
| 976 |
+
color: var(--text-primary);
|
| 977 |
+
margin: 0;
|
| 978 |
+
">${escapeHtml(currentTrace)}</pre>`;
|
| 979 |
+
}
|
| 980 |
+
|
| 981 |
+
function escapeHtml(text) {
|
| 982 |
+
if (!text) return '';
|
| 983 |
+
const div = document.createElement('div');
|
| 984 |
+
div.textContent = text;
|
| 985 |
+
return div.innerHTML;
|
| 986 |
+
}
|
| 987 |
+
|
| 988 |
+
function showTraceModal() {
|
| 989 |
+
renderTrace();
|
| 990 |
+
traceModal.classList.add('active');
|
| 991 |
+
}
|
| 992 |
+
|
| 993 |
+
function hideTraceModal() {
|
| 994 |
+
traceModal.classList.remove('active');
|
| 995 |
+
}
|
| 996 |
+
|
| 997 |
+
// Trace modal event listeners
|
| 998 |
+
traceBtn.addEventListener('click', showTraceModal);
|
| 999 |
+
modalClose.addEventListener('click', hideTraceModal);
|
| 1000 |
+
traceModal.addEventListener('click', (e) => {
|
| 1001 |
+
if (e.target === traceModal) hideTraceModal();
|
| 1002 |
+
});
|
| 1003 |
+
document.addEventListener('keydown', (e) => {
|
| 1004 |
+
if (e.key === 'Escape') hideTraceModal();
|
| 1005 |
+
});
|
| 1006 |
+
|
| 1007 |
+
// Initialize
|
| 1008 |
+
init();
|
| 1009 |
+
</script>
|
| 1010 |
+
</body>
|
| 1011 |
+
</html>
|
| 1012 |
+
|
web_app/uploads/610Report.pdf
ADDED
|
Binary file (76.1 kB). View file
|
|
|