shekkari21 commited on
Commit
64462d2
·
1 Parent(s): e33886d

added session and memory

Browse files
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 xxlimited import Str
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
- ) -> str:
76
- """Run the agent with user input."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- messages = []
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 display_trace(context):
83
- """Display the execution trace of an agent run.
84
 
85
  Args:
86
- context: ExecutionContext to display
 
 
 
87
  """
88
- from .models import Event, Message, ToolCall, ToolResult
89
 
90
- print(f"\n{'='*60}")
91
- print(f"Execution Trace (ID: {context.execution_id})")
92
- print(f"{'='*60}\n")
 
 
93
 
94
  for i, event in enumerate(context.events, 1):
95
- print(f"Step {i} - {event.author.upper()} ({event.timestamp:.2f})")
96
- print(f"{'-'*60}")
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
- print(f" [Message] ({item.role}): {content_preview}")
102
  elif isinstance(item, ToolCall):
103
- print(f" [Tool Call] {item.name}")
104
- print(f" Arguments: {item.arguments}")
105
  elif isinstance(item, ToolResult):
106
  status_marker = "[SUCCESS]" if item.status == "success" else "[ERROR]"
107
- print(f" {status_marker} Tool Result: {item.name} ({item.status})")
108
  if item.content:
109
  content_preview = str(item.content[0])[:100]
110
  if len(str(item.content[0])) > 100:
111
  content_preview += "..."
112
- print(f" Output: {content_preview}")
113
 
114
- print()
115
 
116
- print(f"{'='*60}")
117
- print(f"Final Result: {context.final_result}")
118
- print(f"Total Steps: {context.current_step}")
119
- print(f"{'='*60}\n")
 
 
 
 
 
 
 
 
 
 
 
 
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 have speciality in deleting files",
24
  max_steps=20,
25
  before_tool_callbacks=[approval_callback],
26
  after_tool_callbacks=[search_compressor],
27
  )
28
 
29
- result = await agent.run("delete README.md from the agent_tools directory")
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
- def delete_file(file_path: str) -> str:
324
- """Deletes a file. This action cannot be undone."""
325
- # Only returns message instead of actual deletion (for demo)
326
- return f"File {file_path} has been deleted."
 
 
 
 
 
 
 
 
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">&times;</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