akseljoonas HF Staff commited on
Commit
9120789
·
1 Parent(s): 4197b96

smol cleanup

Browse files
agent/MCP_INTEGRATION.md DELETED
@@ -1,205 +0,0 @@
1
- # MCP Integration for HF Agent
2
-
3
- This agent now supports the Model Context Protocol (MCP), allowing it to connect to and use tools from MCP servers.
4
-
5
- ## Overview
6
-
7
- The MCP integration allows the agent to:
8
- - Connect to multiple MCP servers simultaneously
9
- - Automatically discover and use tools from connected servers
10
- - Execute tool calls through the MCP protocol
11
- - Seamlessly integrate MCP tools with the agent's existing tool system
12
-
13
- ## Architecture
14
-
15
- The integration consists of several components:
16
-
17
- 1. **MCPClient** (`agent/core/mcp_client.py`): Manages connections to MCP servers
18
- 2. **ToolExecutor** (`agent/core/executor.py`): Executes both MCP and local tools
19
- 3. **Config** (`agent/config.py`): Stores MCP server configurations
20
- 4. **Session** (`agent/core/session.py`): Initializes MCP connections and manages lifecycle
21
-
22
- ## Configuration
23
-
24
- To use MCP servers with your agent, add them to your configuration file:
25
-
26
- ```json
27
- {
28
- "model_name": "anthropic/claude-sonnet-4-5-20250929",
29
- "tools": [],
30
- "system_prompt_path": "",
31
- "mcp_servers": [
32
- {
33
- "name": "weather",
34
- "command": "python",
35
- "args": ["path/to/weather_server.py"],
36
- "env": null
37
- },
38
- {
39
- "name": "filesystem",
40
- "command": "node",
41
- "args": ["path/to/filesystem_server.js"],
42
- "env": {
43
- "ALLOWED_PATHS": "/home/user/documents"
44
- }
45
- }
46
- ]
47
- }
48
- ```
49
-
50
- ### Configuration Fields
51
-
52
- - `name`: Unique identifier for the MCP server
53
- - `command`: Command to execute the server (`python`, `node`, etc.)
54
- - `args`: Arguments to pass to the command (path to server script)
55
- - `env`: (Optional) Environment variables for the server process
56
-
57
- ## Usage
58
-
59
- ### Basic Usage
60
-
61
- ```python
62
- import asyncio
63
- from agent.config import Config, load_config
64
- from agent.core.agent_loop import submission_loop
65
-
66
- async def main():
67
- # Load config with MCP servers
68
- config = load_config("config.json")
69
-
70
- # Create queues
71
- submission_queue = asyncio.Queue()
72
- event_queue = asyncio.Queue()
73
-
74
- # Start agent loop (MCP connections initialized automatically)
75
- await submission_loop(submission_queue, event_queue, config)
76
-
77
- if __name__ == "__main__":
78
- asyncio.run(main())
79
- ```
80
-
81
- ### Programmatic Configuration
82
-
83
- ```python
84
- from agent.config import Config, MCPServerConfig
85
-
86
- config = Config(
87
- model_name="anthropic/claude-sonnet-4-5-20250929",
88
- tools=[],
89
- system_prompt_path="",
90
- mcp_servers=[
91
- MCPServerConfig(
92
- name="weather",
93
- command="python",
94
- args=["weather_server.py"],
95
- env=None
96
- )
97
- ]
98
- )
99
- ```
100
-
101
- ## How It Works
102
-
103
- 1. **Initialization**: When the agent loop starts, it calls `session.initialize_mcp()`
104
- 2. **Connection**: The session connects to all configured MCP servers
105
- 3. **Tool Discovery**: Tools from all servers are discovered and added to the agent's tool list
106
- 4. **Tool Naming**: MCP tools are prefixed with their server name (e.g., `weather__get_forecast`)
107
- 5. **Execution**: When the LLM calls a tool, the ToolExecutor routes it to the appropriate MCP server
108
- 6. **Cleanup**: When the agent shuts down, all MCP connections are cleaned up properly
109
-
110
- ## Tool Naming Convention
111
-
112
- MCP tools are automatically prefixed with their server name to avoid conflicts:
113
-
114
- - Original tool: `get_forecast`
115
- - MCP tool name: `weather__get_forecast`
116
-
117
- This ensures that tools from different servers don't conflict, even if they have the same name.
118
-
119
- ## Example: Creating a Simple MCP Server
120
-
121
- Here's a minimal example of an MCP server (save as `calculator_server.py`):
122
-
123
- ```python
124
- import asyncio
125
- from mcp.server import Server, stdio_server
126
- from mcp.types import Tool, TextContent
127
-
128
- app = Server("calculator")
129
-
130
- @app.list_tools()
131
- async def list_tools() -> list[Tool]:
132
- return [
133
- Tool(
134
- name="add",
135
- description="Add two numbers",
136
- inputSchema={
137
- "type": "object",
138
- "properties": {
139
- "a": {"type": "number"},
140
- "b": {"type": "number"}
141
- },
142
- "required": ["a", "b"]
143
- }
144
- )
145
- ]
146
-
147
- @app.call_tool()
148
- async def call_tool(name: str, arguments: dict) -> list[TextContent]:
149
- if name == "add":
150
- result = arguments["a"] + arguments["b"]
151
- return [TextContent(type="text", text=str(result))]
152
-
153
- raise ValueError(f"Unknown tool: {name}")
154
-
155
- async def main():
156
- async with stdio_server() as (read_stream, write_stream):
157
- await app.run(read_stream, write_stream, app.create_initialization_options())
158
-
159
- if __name__ == "__main__":
160
- asyncio.run(main())
161
- ```
162
-
163
- ## Troubleshooting
164
-
165
- ### Server Connection Issues
166
-
167
- If you see errors connecting to an MCP server:
168
-
169
- 1. Check that the server script path is correct
170
- 2. Ensure the command (`python`, `node`) is in your PATH
171
- 3. Verify the server script is executable
172
- 4. Check server logs for initialization errors
173
-
174
- ### Tool Not Found
175
-
176
- If the agent can't find an MCP tool:
177
-
178
- 1. Verify the server is connected (check startup logs)
179
- 2. Check tool naming (should be `servername__toolname`)
180
- 3. Ensure the server properly implements `list_tools()`
181
-
182
- ### Performance Considerations
183
-
184
- - MCP server initialization happens once at startup
185
- - Tool calls are asynchronous and don't block the agent
186
- - Multiple servers can be used simultaneously
187
- - Consider using local tools for high-frequency operations
188
-
189
- ## Best Practices
190
-
191
- 1. **Unique Server Names**: Give each MCP server a unique, descriptive name
192
- 2. **Error Handling**: MCP connection failures are logged but don't crash the agent
193
- 3. **Resource Cleanup**: Always let the agent shut down gracefully to cleanup connections
194
- 4. **Testing**: Test MCP servers independently before integrating them
195
- 5. **Security**: Be cautious with file system and network access in MCP servers
196
-
197
- ## Future Enhancements
198
-
199
- Potential improvements to consider:
200
-
201
- - Dynamic server addition/removal during runtime
202
- - Server health monitoring and auto-reconnection
203
- - Tool caching and performance optimization
204
- - Support for MCP resources and prompts
205
- - Rate limiting and timeout configuration
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
agent/codex_agent_demo.py DELETED
@@ -1,470 +0,0 @@
1
- """
2
- Minimum Viable Implementation of Codex Agent Loop in Python
3
-
4
- This demonstrates the core architecture patterns from codex-rs:
5
- - Async submission loop (like submission_loop in codex.rs)
6
- - Context manager for conversation history
7
- - Channel-based communication (submissions in, events out)
8
- - Handler pattern for operations
9
- """
10
-
11
- import asyncio
12
- from dataclasses import dataclass, field
13
- from datetime import datetime
14
- from enum import Enum
15
- from typing import Any, Dict, List, Optional
16
-
17
- # ============================================================================
18
- # PROTOCOL TYPES (ResponseItem equivalents)
19
- # ============================================================================
20
-
21
-
22
- class MessageRole(Enum):
23
- SYSTEM = "system"
24
- USER = "user"
25
- ASSISTANT = "assistant"
26
-
27
-
28
- @dataclass
29
- class Message:
30
- role: MessageRole
31
- content: str
32
- timestamp: datetime = field(default_factory=datetime.now)
33
-
34
-
35
- @dataclass
36
- class ToolCall:
37
- call_id: str
38
- tool_name: str
39
- arguments: Dict[str, Any]
40
-
41
-
42
- @dataclass
43
- class ToolOutput:
44
- call_id: str
45
- content: str
46
- success: bool = True
47
-
48
-
49
- # ============================================================================
50
- # CONTEXT MANAGER (like context_manager/history.rs)
51
- # ============================================================================
52
-
53
-
54
- class ContextManager:
55
- """
56
- Manages conversation history with normalization and truncation.
57
- Based on codex-rs/core/src/context_manager/history.rs
58
- """
59
-
60
- def __init__(self, max_history_length: int = 1000):
61
- self.items: List[Any] = [] # Oldest → Newest
62
- self.token_count: int = 0
63
- self.max_history_length = max_history_length
64
-
65
- def record_items(self, items: List[Any]) -> None:
66
- """Record new items to history (like record_items in history.rs:41)"""
67
- for item in items:
68
- # Filter and process items
69
- if self._is_api_message(item):
70
- processed = self._process_item(item)
71
- self.items.append(processed)
72
-
73
- def _is_api_message(self, item: Any) -> bool:
74
- """Filter out system messages (like is_api_message in history.rs:157)"""
75
- if isinstance(item, Message):
76
- return item.role != MessageRole.SYSTEM
77
- return isinstance(item, (ToolCall, ToolOutput))
78
-
79
- def _process_item(self, item: Any) -> Any:
80
- """Process item before adding (like process_item in history.rs:119)"""
81
- # Truncate long outputs
82
- if isinstance(item, ToolOutput):
83
- if len(item.content) > 2000:
84
- item.content = item.content[:2000] + "...[truncated]"
85
- return item
86
-
87
- def get_history_for_prompt(self) -> List[Any]:
88
- """
89
- Get normalized history ready for model
90
- (like get_history_for_prompt in history.rs:65)
91
- """
92
- self._normalize_history()
93
- return self.items.copy()
94
-
95
- def _normalize_history(self) -> None:
96
- """
97
- Enforce invariants (like normalize_history in history.rs:102):
98
- 1. Every tool call has corresponding output
99
- 2. Every output has corresponding call
100
- """
101
- # Build mapping of call_id → call
102
- calls = {}
103
- outputs = {}
104
-
105
- for item in self.items:
106
- if isinstance(item, ToolCall):
107
- calls[item.call_id] = item
108
- elif isinstance(item, ToolOutput):
109
- outputs[item.call_id] = item
110
-
111
- # Remove orphan outputs (no matching call)
112
- self.items = [
113
- item
114
- for item in self.items
115
- if not isinstance(item, ToolOutput) or item.call_id in calls
116
- ]
117
-
118
- # Add missing outputs for calls (create synthetic outputs)
119
- for call_id, call in calls.items():
120
- if call_id not in outputs:
121
- self.items.append(
122
- ToolOutput(
123
- call_id=call_id, content="[No output recorded]", success=False
124
- )
125
- )
126
-
127
- def remove_first_item(self) -> None:
128
- """Remove oldest item for compaction (like remove_first_item in history.rs:71)"""
129
- if self.items:
130
- removed = self.items.pop(0)
131
- # Also remove corresponding pair if needed
132
- if isinstance(removed, ToolCall):
133
- self.items = [
134
- item
135
- for item in self.items
136
- if not (
137
- isinstance(item, ToolOutput) and item.call_id == removed.call_id
138
- )
139
- ]
140
- elif isinstance(removed, ToolOutput):
141
- self.items = [
142
- item
143
- for item in self.items
144
- if not (
145
- isinstance(item, ToolCall) and item.call_id == removed.call_id
146
- )
147
- ]
148
-
149
- def compact(self, target_size: int) -> None:
150
- """Remove old items until we're under target size"""
151
- while len(self.items) > target_size:
152
- self.remove_first_item()
153
-
154
-
155
- # ============================================================================
156
- # OPERATIONS (like Op enum in codex.rs)
157
- # ============================================================================
158
-
159
-
160
- class OpType(Enum):
161
- USER_INPUT = "user_input"
162
- EXEC_APPROVAL = "exec_approval"
163
- INTERRUPT = "interrupt"
164
- UNDO = "undo"
165
- COMPACT = "compact"
166
- SHUTDOWN = "shutdown"
167
-
168
-
169
- @dataclass
170
- class Operation:
171
- op_type: OpType
172
- data: Optional[Dict[str, Any]] = None
173
-
174
-
175
- @dataclass
176
- class Submission:
177
- id: str
178
- operation: Operation
179
-
180
-
181
- # ============================================================================
182
- # EVENTS (like Event in codex-rs)
183
- # ============================================================================
184
-
185
-
186
- @dataclass
187
- class Event:
188
- event_type: str
189
- data: Optional[Dict[str, Any]] = None
190
-
191
-
192
- # ============================================================================
193
- # SESSION STATE (like Session in codex.rs)
194
- # ============================================================================
195
-
196
-
197
- class Session:
198
- """
199
- Maintains agent session state
200
- Similar to Session in codex-rs/core/src/codex.rs
201
- """
202
-
203
- def __init__(self, event_queue: asyncio.Queue):
204
- self.context_manager = ContextManager(tool_specs=[])
205
- self.event_queue = event_queue
206
- self.is_running = True
207
- self.current_task: Optional[asyncio.Task] = None
208
-
209
- async def send_event(self, event: Event) -> None:
210
- """Send event back to client"""
211
- await self.event_queue.put(event)
212
-
213
- def interrupt(self) -> None:
214
- """Interrupt current running task"""
215
- if self.current_task and not self.current_task.done():
216
- self.current_task.cancel()
217
-
218
-
219
- # ============================================================================
220
- # OPERATION HANDLERS (like handlers module in codex.rs:1343)
221
- # ============================================================================
222
-
223
-
224
- class Handlers:
225
- """Handler functions for each operation type"""
226
-
227
- @staticmethod
228
- async def user_input(session: Session, text: str) -> None:
229
- """Handle user input (like user_input_or_turn in codex.rs:1291)"""
230
- # Add user message to history
231
- user_msg = Message(role=MessageRole.USER, content=text)
232
- session.context_manager.record_items([user_msg])
233
-
234
- # Send event that we're processing
235
- await session.send_event(
236
- Event(event_type="processing", data={"message": "Processing user input"})
237
- )
238
-
239
- # Simulate agent processing
240
- await asyncio.sleep(0.1)
241
-
242
- # Generate mock assistant response
243
- assistant_msg = Message(
244
- role=MessageRole.ASSISTANT, content=f"I received: {text}"
245
- )
246
- session.context_manager.record_items([assistant_msg])
247
-
248
- # Simulate tool call
249
- tool_call = ToolCall(
250
- call_id="call_123", tool_name="bash", arguments={"command": "echo 'hello'"}
251
- )
252
- session.context_manager.record_items([tool_call])
253
-
254
- # Simulate tool execution
255
- await asyncio.sleep(0.1)
256
-
257
- tool_output = ToolOutput(call_id="call_123", content="hello\n", success=True)
258
- session.context_manager.record_items([tool_output])
259
-
260
- # Send completion event
261
- await session.send_event(
262
- Event(
263
- event_type="turn_complete",
264
- data={"history_size": len(session.context_manager.items)},
265
- )
266
- )
267
-
268
- @staticmethod
269
- async def interrupt(session: Session) -> None:
270
- """Handle interrupt (like interrupt in codex.rs:1266)"""
271
- session.interrupt()
272
- await session.send_event(Event(event_type="interrupted"))
273
-
274
- @staticmethod
275
- async def compact(session: Session) -> None:
276
- """Handle compact (like compact in codex.rs:1317)"""
277
- old_size = len(session.context_manager.items)
278
- session.context_manager.compact(target_size=10)
279
- new_size = len(session.context_manager.items)
280
-
281
- await session.send_event(
282
- Event(
283
- event_type="compacted",
284
- data={"removed": old_size - new_size, "remaining": new_size},
285
- )
286
- )
287
-
288
- @staticmethod
289
- async def undo(session: Session) -> None:
290
- """Handle undo (like undo in codex.rs:1314)"""
291
- # Remove last user turn and all following items
292
- # Simplified: just remove last 2 items
293
- for _ in range(min(2, len(session.context_manager.items))):
294
- session.context_manager.items.pop()
295
-
296
- await session.send_event(Event(event_type="undo_complete"))
297
-
298
- @staticmethod
299
- async def shutdown(session: Session) -> bool:
300
- """Handle shutdown (like shutdown in codex.rs:1329)"""
301
- session.is_running = False
302
- await session.send_event(Event(event_type="shutdown"))
303
- return True
304
-
305
-
306
- # ============================================================================
307
- # MAIN AGENT LOOP (like submission_loop in codex.rs:1259)
308
- # ============================================================================
309
-
310
-
311
- async def submission_loop(
312
- submission_queue: asyncio.Queue, event_queue: asyncio.Queue
313
- ) -> None:
314
- """
315
- Main agent loop - processes submissions and dispatches to handlers.
316
- This is the core of the agent (like submission_loop in codex.rs:1259-1340)
317
- """
318
- session = Session(event_queue)
319
-
320
- print("🤖 Agent loop started")
321
-
322
- # Main processing loop
323
- while session.is_running:
324
- try:
325
- # Wait for next submission (like rx_sub.recv() in codex.rs:1262)
326
- submission = await submission_queue.get()
327
-
328
- print(f"📨 Received: {submission.operation.op_type.value}")
329
-
330
- # Dispatch to handler based on operation type
331
- # (like match in codex.rs:1264-1337)
332
- op = submission.operation
333
-
334
- if op.op_type == OpType.USER_INPUT:
335
- text = op.data.get("text", "") if op.data else ""
336
- await Handlers.user_input(session, text)
337
-
338
- elif op.op_type == OpType.INTERRUPT:
339
- await Handlers.interrupt(session)
340
-
341
- elif op.op_type == OpType.COMPACT:
342
- await Handlers.compact(session)
343
-
344
- elif op.op_type == OpType.UNDO:
345
- await Handlers.undo(session)
346
-
347
- elif op.op_type == OpType.SHUTDOWN:
348
- if await Handlers.shutdown(session):
349
- break
350
-
351
- else:
352
- print(f"⚠️ Unknown operation: {op.op_type}")
353
-
354
- except asyncio.CancelledError:
355
- break
356
- except Exception as e:
357
- print(f"❌ Error in agent loop: {e}")
358
- await session.send_event(Event(event_type="error", data={"error": str(e)}))
359
-
360
- print("🛑 Agent loop exited")
361
-
362
-
363
- # ============================================================================
364
- # CODEX INTERFACE (like Codex struct in codex.rs:154)
365
- # ============================================================================
366
-
367
-
368
- class Codex:
369
- """
370
- Main interface to the agent (like Codex in codex.rs:154-246)
371
- Provides submit() and next_event() methods
372
- """
373
-
374
- def __init__(self):
375
- self.submission_queue = asyncio.Queue()
376
- self.event_queue = asyncio.Queue()
377
- self.agent_task: Optional[asyncio.Task] = None
378
- self.submission_counter = 0
379
-
380
- async def spawn(self) -> None:
381
- """Spawn the agent loop (like Codex::spawn in codex.rs:156)"""
382
- self.agent_task = asyncio.create_task(
383
- submission_loop(self.submission_queue, self.event_queue)
384
- )
385
-
386
- async def submit(self, operation: Operation) -> str:
387
- """Submit operation to agent (like Codex::submit in codex.rs:218)"""
388
- self.submission_counter += 1
389
- submission = Submission(
390
- id=f"sub_{self.submission_counter}", operation=operation
391
- )
392
- await self.submission_queue.put(submission)
393
- return submission.id
394
-
395
- async def next_event(self) -> Optional[Event]:
396
- """Get next event from agent (like Codex::next_event in codex.rs:238)"""
397
- try:
398
- return await asyncio.wait_for(self.event_queue.get(), timeout=1.0)
399
- except asyncio.TimeoutError:
400
- return None
401
-
402
- async def shutdown(self) -> None:
403
- """Shutdown the agent"""
404
- await self.submit(Operation(op_type=OpType.SHUTDOWN))
405
- if self.agent_task:
406
- await self.agent_task
407
-
408
-
409
- # ============================================================================
410
- # DEMO / EXAMPLE USAGE
411
- # ============================================================================
412
-
413
-
414
- async def main():
415
- """Demo of the agent system"""
416
- print("=" * 60)
417
- print("Codex Agent Loop Demo (Python MVP)")
418
- print("=" * 60)
419
-
420
- # Create and spawn agent
421
- codex = Codex()
422
- await codex.spawn()
423
-
424
- # Submit some operations
425
- print("\n1️⃣ Submitting user input...")
426
- await codex.submit(
427
- Operation(op_type=OpType.USER_INPUT, data={"text": "Hello, agent!"})
428
- )
429
-
430
- # Receive events
431
- for _ in range(3):
432
- event = await codex.next_event()
433
- if event:
434
- print(f" ✅ Event: {event.event_type} - {event.data}")
435
-
436
- print("\n2️⃣ Submitting another input...")
437
- await codex.submit(
438
- Operation(op_type=OpType.USER_INPUT, data={"text": "What's the weather?"})
439
- )
440
-
441
- for _ in range(3):
442
- event = await codex.next_event()
443
- if event:
444
- print(f" ✅ Event: {event.event_type} - {event.data}")
445
-
446
- print("\n3️⃣ Compacting history...")
447
- await codex.submit(Operation(op_type=OpType.COMPACT))
448
-
449
- event = await codex.next_event()
450
- if event:
451
- print(f" ✅ Event: {event.event_type} - {event.data}")
452
-
453
- print("\n4️⃣ Undoing last turn...")
454
- await codex.submit(Operation(op_type=OpType.UNDO))
455
-
456
- event = await codex.next_event()
457
- if event:
458
- print(f" ✅ Event: {event.event_type}")
459
-
460
- # Shutdown
461
- print("\n5️⃣ Shutting down...")
462
- await codex.shutdown()
463
-
464
- print("\n" + "=" * 60)
465
- print("Demo complete!")
466
- print("=" * 60)
467
-
468
-
469
- if __name__ == "__main__":
470
- asyncio.run(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
agent/utils/__init__.py CHANGED
@@ -1,7 +1,3 @@
1
  """
2
  Utility functions and helpers
3
  """
4
-
5
- from agent.utils.logging import setup_logger
6
-
7
- __all__ = ["setup_logger"]
 
1
  """
2
  Utility functions and helpers
3
  """
 
 
 
 
agent/utils/logging.py DELETED
@@ -1,40 +0,0 @@
1
- """
2
- Logging utilities
3
- """
4
-
5
- import logging
6
- import sys
7
- from pathlib import Path
8
- from typing import Optional
9
-
10
-
11
- def setup_logger(
12
- name: str = "hf_agent", level: int = logging.INFO, log_file: Optional[Path] = None
13
- ) -> logging.Logger:
14
- """Setup and configure logger"""
15
-
16
- logger = logging.getLogger(name)
17
- logger.setLevel(level)
18
-
19
- # Remove existing handlers
20
- logger.handlers = []
21
-
22
- # Console handler
23
- console_handler = logging.StreamHandler(sys.stdout)
24
- console_handler.setLevel(level)
25
- console_format = logging.Formatter(
26
- "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
27
- datefmt="%Y-%m-%d %H:%M:%S",
28
- )
29
- console_handler.setFormatter(console_format)
30
- logger.addHandler(console_handler)
31
-
32
- # File handler if log_file specified
33
- if log_file:
34
- log_file.parent.mkdir(parents=True, exist_ok=True)
35
- file_handler = logging.FileHandler(log_file)
36
- file_handler.setLevel(level)
37
- file_handler.setFormatter(console_format)
38
- logger.addHandler(file_handler)
39
-
40
- return logger