nc1709 1.15.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. nc1709/__init__.py +13 -0
  2. nc1709/agent/__init__.py +36 -0
  3. nc1709/agent/core.py +505 -0
  4. nc1709/agent/mcp_bridge.py +245 -0
  5. nc1709/agent/permissions.py +298 -0
  6. nc1709/agent/tools/__init__.py +21 -0
  7. nc1709/agent/tools/base.py +440 -0
  8. nc1709/agent/tools/bash_tool.py +367 -0
  9. nc1709/agent/tools/file_tools.py +454 -0
  10. nc1709/agent/tools/notebook_tools.py +516 -0
  11. nc1709/agent/tools/search_tools.py +322 -0
  12. nc1709/agent/tools/task_tool.py +284 -0
  13. nc1709/agent/tools/web_tools.py +555 -0
  14. nc1709/agents/__init__.py +17 -0
  15. nc1709/agents/auto_fix.py +506 -0
  16. nc1709/agents/test_generator.py +507 -0
  17. nc1709/checkpoints.py +372 -0
  18. nc1709/cli.py +3380 -0
  19. nc1709/cli_ui.py +1080 -0
  20. nc1709/cognitive/__init__.py +149 -0
  21. nc1709/cognitive/anticipation.py +594 -0
  22. nc1709/cognitive/context_engine.py +1046 -0
  23. nc1709/cognitive/council.py +824 -0
  24. nc1709/cognitive/learning.py +761 -0
  25. nc1709/cognitive/router.py +583 -0
  26. nc1709/cognitive/system.py +519 -0
  27. nc1709/config.py +155 -0
  28. nc1709/custom_commands.py +300 -0
  29. nc1709/executor.py +333 -0
  30. nc1709/file_controller.py +354 -0
  31. nc1709/git_integration.py +308 -0
  32. nc1709/github_integration.py +477 -0
  33. nc1709/image_input.py +446 -0
  34. nc1709/linting.py +519 -0
  35. nc1709/llm_adapter.py +667 -0
  36. nc1709/logger.py +192 -0
  37. nc1709/mcp/__init__.py +18 -0
  38. nc1709/mcp/client.py +370 -0
  39. nc1709/mcp/manager.py +407 -0
  40. nc1709/mcp/protocol.py +210 -0
  41. nc1709/mcp/server.py +473 -0
  42. nc1709/memory/__init__.py +20 -0
  43. nc1709/memory/embeddings.py +325 -0
  44. nc1709/memory/indexer.py +474 -0
  45. nc1709/memory/sessions.py +432 -0
  46. nc1709/memory/vector_store.py +451 -0
  47. nc1709/models/__init__.py +86 -0
  48. nc1709/models/detector.py +377 -0
  49. nc1709/models/formats.py +315 -0
  50. nc1709/models/manager.py +438 -0
  51. nc1709/models/registry.py +497 -0
  52. nc1709/performance/__init__.py +343 -0
  53. nc1709/performance/cache.py +705 -0
  54. nc1709/performance/pipeline.py +611 -0
  55. nc1709/performance/tiering.py +543 -0
  56. nc1709/plan_mode.py +362 -0
  57. nc1709/plugins/__init__.py +17 -0
  58. nc1709/plugins/agents/__init__.py +18 -0
  59. nc1709/plugins/agents/django_agent.py +912 -0
  60. nc1709/plugins/agents/docker_agent.py +623 -0
  61. nc1709/plugins/agents/fastapi_agent.py +887 -0
  62. nc1709/plugins/agents/git_agent.py +731 -0
  63. nc1709/plugins/agents/nextjs_agent.py +867 -0
  64. nc1709/plugins/base.py +359 -0
  65. nc1709/plugins/manager.py +411 -0
  66. nc1709/plugins/registry.py +337 -0
  67. nc1709/progress.py +443 -0
  68. nc1709/prompts/__init__.py +22 -0
  69. nc1709/prompts/agent_system.py +180 -0
  70. nc1709/prompts/task_prompts.py +340 -0
  71. nc1709/prompts/unified_prompt.py +133 -0
  72. nc1709/reasoning_engine.py +541 -0
  73. nc1709/remote_client.py +266 -0
  74. nc1709/shell_completions.py +349 -0
  75. nc1709/slash_commands.py +649 -0
  76. nc1709/task_classifier.py +408 -0
  77. nc1709/version_check.py +177 -0
  78. nc1709/web/__init__.py +8 -0
  79. nc1709/web/server.py +950 -0
  80. nc1709/web/templates/index.html +1127 -0
  81. nc1709-1.15.4.dist-info/METADATA +858 -0
  82. nc1709-1.15.4.dist-info/RECORD +86 -0
  83. nc1709-1.15.4.dist-info/WHEEL +5 -0
  84. nc1709-1.15.4.dist-info/entry_points.txt +2 -0
  85. nc1709-1.15.4.dist-info/licenses/LICENSE +9 -0
  86. nc1709-1.15.4.dist-info/top_level.txt +1 -0
nc1709/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ """
2
+ NC1709 CLI - Your AI coding partner that brings your code to life
3
+ Version: 1.15.3
4
+ Author: Lafzusa Corp
5
+ License: Proprietary
6
+ """
7
+
8
+ __version__ = "1.15.4"
9
+ __author__ = "Lafzusa Corp"
10
+
11
+ from .cli import main
12
+
13
+ __all__ = ["main"]
@@ -0,0 +1,36 @@
1
+ """
2
+ NC1709 Agent Module
3
+
4
+ Provides an agentic architecture similar to Claude Code with:
5
+ - Tool-based execution (Read, Write, Edit, Grep, Glob, Bash, etc.)
6
+ - Sub-agent spawning for complex tasks
7
+ - MCP integration for extended capabilities
8
+ - Permission system for tool execution
9
+ - Real-time visual feedback
10
+ """
11
+
12
+ from .core import Agent, AgentConfig, create_agent
13
+ from .tools.base import Tool, ToolResult, ToolRegistry, ToolPermission, ToolParameter
14
+ from .permissions import PermissionManager, PermissionPolicy, PermissionConfig
15
+ from .mcp_bridge import MCPBridge, MCPTool, integrate_mcp_with_agent
16
+
17
+ __all__ = [
18
+ # Core
19
+ "Agent",
20
+ "AgentConfig",
21
+ "create_agent",
22
+ # Tools
23
+ "Tool",
24
+ "ToolResult",
25
+ "ToolRegistry",
26
+ "ToolPermission",
27
+ "ToolParameter",
28
+ # Permissions
29
+ "PermissionManager",
30
+ "PermissionPolicy",
31
+ "PermissionConfig",
32
+ # MCP
33
+ "MCPBridge",
34
+ "MCPTool",
35
+ "integrate_mcp_with_agent",
36
+ ]
nc1709/agent/core.py ADDED
@@ -0,0 +1,505 @@
1
+ """
2
+ Agent Core
3
+
4
+ The main Agent class that implements the agentic execution loop:
5
+ 1. Receive user request
6
+ 2. Plan and decide on tool calls
7
+ 3. Execute tools with visual feedback
8
+ 4. Process results and continue or complete
9
+ """
10
+
11
+ import json
12
+ import re
13
+ from dataclasses import dataclass, field
14
+ from typing import Any, Callable, Dict, List, Optional, TYPE_CHECKING
15
+ from enum import Enum
16
+
17
+ from .tools.base import Tool, ToolResult, ToolRegistry, ToolPermission, get_default_registry
18
+ from .tools.file_tools import register_file_tools
19
+ from .tools.search_tools import register_search_tools
20
+ from .tools.bash_tool import register_bash_tools
21
+ from .tools.task_tool import register_task_tools, TaskTool
22
+ from .tools.web_tools import register_web_tools
23
+ from .tools.notebook_tools import register_notebook_tools
24
+
25
+ # Import visual feedback components
26
+ try:
27
+ from ..cli_ui import (
28
+ ActionSpinner, Color, Icons,
29
+ status, thinking, success, error, warning, info,
30
+ log_action
31
+ )
32
+ HAS_CLI_UI = True
33
+ except ImportError:
34
+ HAS_CLI_UI = False
35
+
36
+
37
+ class AgentState(Enum):
38
+ """States of the agent execution"""
39
+ IDLE = "idle"
40
+ THINKING = "thinking"
41
+ EXECUTING_TOOL = "executing_tool"
42
+ WAITING_APPROVAL = "waiting_approval"
43
+ COMPLETED = "completed"
44
+ ERROR = "error"
45
+
46
+
47
+ @dataclass
48
+ class AgentConfig:
49
+ """Configuration for an Agent"""
50
+ max_iterations: int = 50
51
+ max_tool_retries: int = 3
52
+ max_same_error_retries: int = 2 # Max times to retry the same failing command
53
+ tool_permissions: Dict[str, ToolPermission] = field(default_factory=dict)
54
+ auto_approve_tools: List[str] = field(default_factory=lambda: [
55
+ "Read", "Glob", "Grep", "TodoWrite"
56
+ ])
57
+ require_approval_tools: List[str] = field(default_factory=lambda: [
58
+ "Write", "Edit", "Bash", "Task", "WebFetch"
59
+ ])
60
+ show_tool_output: bool = True
61
+ verbose: bool = False
62
+
63
+
64
+ @dataclass
65
+ class ToolCall:
66
+ """Represents a tool call parsed from LLM output"""
67
+ name: str
68
+ parameters: Dict[str, Any]
69
+ raw_text: str = ""
70
+
71
+
72
+ class Agent:
73
+ """
74
+ Main Agent class implementing the agentic execution loop.
75
+
76
+ The agent:
77
+ 1. Takes a user request
78
+ 2. Uses an LLM to decide what tools to call
79
+ 3. Executes tools with permission checks
80
+ 4. Provides visual feedback
81
+ 5. Continues until task is complete
82
+ """
83
+
84
+ SYSTEM_PROMPT = """You are NC1709, an AI assistant with access to tools for completing tasks.
85
+
86
+ You have access to these tools:
87
+ {tools_description}
88
+
89
+ ## How to use tools
90
+
91
+ To use a tool, include a tool call in your response using this exact format:
92
+ ```tool
93
+ {{"tool": "ToolName", "parameters": {{"param1": "value1", "param2": "value2"}}}}
94
+ ```
95
+
96
+ ## Guidelines
97
+
98
+ 1. **Read before writing**: Always read files before modifying them
99
+ 2. **Be precise**: Use exact file paths and specific parameters
100
+ 3. **Explain your actions**: Briefly explain what you're doing and why
101
+ 4. **Handle errors**: If a tool fails, try a DIFFERENT approach instead of repeating the same command
102
+ 5. **Complete the task**: Keep working until the task is fully complete
103
+ 6. **Ask if unclear**: Use AskUser if you need clarification
104
+
105
+ ## Building New Projects - IMPORTANT
106
+
107
+ When creating a new project or application:
108
+
109
+ 1. **Always create a project directory first**:
110
+ - Create a dedicated directory for the project (e.g., `my_project/`)
111
+ - ALL project files go inside this directory
112
+ - Example: For a "quantum simulator", create `quantum_simulator/` first
113
+
114
+ 2. **Set up Python environment properly**:
115
+ - Create a virtual environment: `python3 -m venv <project_dir>/venv`
116
+ - Install packages using: `<project_dir>/venv/bin/pip install <package>`
117
+ - Run scripts using: `<project_dir>/venv/bin/python <script.py>`
118
+ - NEVER use bare `pip install` - always use the venv pip
119
+
120
+ 3. **Project structure example**:
121
+ ```
122
+ project_name/
123
+ ├── venv/ # Virtual environment
124
+ ├── requirements.txt # Dependencies
125
+ ├── main.py # Entry point
126
+ ├── app.py # Web app (if applicable)
127
+ ├── static/ # Static files
128
+ └── templates/ # HTML templates
129
+ ```
130
+
131
+ 4. **After writing files, verify they exist and have content**
132
+
133
+ ## Error Recovery
134
+
135
+ - If a command fails 2 times with the same error, TRY A DIFFERENT APPROACH
136
+ - Don't keep repeating `pip install` if it fails - check the Python environment
137
+ - Use absolute paths when relative paths fail
138
+ - If a module is not found, verify the correct Python interpreter is being used
139
+
140
+ ## Current context
141
+
142
+ Working directory: {cwd}
143
+ """
144
+
145
+ def __init__(
146
+ self,
147
+ llm=None,
148
+ config: AgentConfig = None,
149
+ registry: ToolRegistry = None,
150
+ parent_agent: "Agent" = None,
151
+ ):
152
+ """Initialize the agent
153
+
154
+ Args:
155
+ llm: LLM adapter for generating responses
156
+ config: Agent configuration
157
+ registry: Tool registry (creates default if None)
158
+ parent_agent: Parent agent if this is a sub-agent
159
+ """
160
+ self.llm = llm
161
+ self.config = config or AgentConfig()
162
+ self.registry = registry or self._create_default_registry()
163
+ self.parent_agent = parent_agent
164
+
165
+ self.state = AgentState.IDLE
166
+ self.iteration_count = 0
167
+ self.conversation_history: List[Dict[str, str]] = []
168
+ self.tool_results: List[ToolResult] = []
169
+
170
+ # Visual feedback
171
+ self._spinner: Optional[ActionSpinner] = None
172
+
173
+ # Loop detection - track failed commands to avoid repeating
174
+ self._failed_commands: Dict[str, int] = {} # command_signature -> failure_count
175
+ self._last_error: Optional[str] = None
176
+
177
+ # Apply permission settings
178
+ self._apply_permission_config()
179
+
180
+ def _create_default_registry(self) -> ToolRegistry:
181
+ """Create and populate default tool registry"""
182
+ registry = ToolRegistry()
183
+
184
+ # Register all built-in tools
185
+ register_file_tools(registry)
186
+ register_search_tools(registry)
187
+ register_bash_tools(registry)
188
+ task_tool = register_task_tools(registry, parent_agent=self)
189
+ register_web_tools(registry)
190
+ register_notebook_tools(registry)
191
+
192
+ return registry
193
+
194
+ def _apply_permission_config(self) -> None:
195
+ """Apply permission configuration to registry"""
196
+ # Set auto-approve tools
197
+ for tool_name in self.config.auto_approve_tools:
198
+ self.registry.set_permission(tool_name, ToolPermission.AUTO)
199
+
200
+ # Set require-approval tools
201
+ for tool_name in self.config.require_approval_tools:
202
+ self.registry.set_permission(tool_name, ToolPermission.ASK)
203
+
204
+ # Apply custom overrides
205
+ for tool_name, permission in self.config.tool_permissions.items():
206
+ self.registry.set_permission(tool_name, permission)
207
+
208
+ def run(self, user_request: str) -> str:
209
+ """
210
+ Run the agent on a user request.
211
+
212
+ Args:
213
+ user_request: The user's request or task
214
+
215
+ Returns:
216
+ Final response or result
217
+ """
218
+ self.state = AgentState.THINKING
219
+ self.iteration_count = 0
220
+ self.conversation_history = []
221
+ self.tool_results = []
222
+
223
+ # Build system prompt with tools
224
+ import os
225
+ system_prompt = self.SYSTEM_PROMPT.format(
226
+ tools_description=self.registry.get_tools_prompt(),
227
+ cwd=os.getcwd(),
228
+ )
229
+
230
+ # Add user request
231
+ self.conversation_history.append({
232
+ "role": "system",
233
+ "content": system_prompt,
234
+ })
235
+ self.conversation_history.append({
236
+ "role": "user",
237
+ "content": user_request,
238
+ })
239
+
240
+ # Main execution loop
241
+ while self.iteration_count < self.config.max_iterations:
242
+ self.iteration_count += 1
243
+
244
+ if HAS_CLI_UI:
245
+ thinking(f"Iteration {self.iteration_count}...")
246
+
247
+ try:
248
+ # Get LLM response
249
+ response = self._get_llm_response()
250
+
251
+ # Parse for tool calls
252
+ tool_calls = self._parse_tool_calls(response)
253
+
254
+ if not tool_calls:
255
+ # No tool calls - agent is done or providing final response
256
+ self.state = AgentState.COMPLETED
257
+ return self._clean_response(response)
258
+
259
+ # Execute tool calls
260
+ all_results = []
261
+ for tool_call in tool_calls:
262
+ result = self._execute_tool_call(tool_call)
263
+ all_results.append(result)
264
+ self.tool_results.append(result)
265
+
266
+ # Add results to conversation
267
+ results_text = self._format_tool_results(all_results)
268
+ self.conversation_history.append({
269
+ "role": "assistant",
270
+ "content": response,
271
+ })
272
+ self.conversation_history.append({
273
+ "role": "user",
274
+ "content": f"Tool results:\n{results_text}\n\nContinue with the task.",
275
+ })
276
+
277
+ except Exception as e:
278
+ self.state = AgentState.ERROR
279
+ if HAS_CLI_UI:
280
+ error(f"Agent error: {e}")
281
+ return f"Error during execution: {e}"
282
+
283
+ # Max iterations reached
284
+ self.state = AgentState.COMPLETED
285
+ if HAS_CLI_UI:
286
+ warning(f"Reached maximum iterations ({self.config.max_iterations})")
287
+ return "Task incomplete - reached maximum iterations."
288
+
289
+ def _get_llm_response(self) -> str:
290
+ """Get response from LLM"""
291
+ if self.llm is None:
292
+ raise ValueError("No LLM configured for agent")
293
+
294
+ # Build messages for LLM
295
+ messages = self.conversation_history.copy()
296
+
297
+ # Get completion
298
+ response = self.llm.chat(messages)
299
+ return response
300
+
301
+ def _parse_tool_calls(self, response: str) -> List[ToolCall]:
302
+ """Parse tool calls from LLM response"""
303
+ tool_calls = []
304
+
305
+ # Pattern 1: ```tool ... ``` blocks
306
+ pattern = r"```tool\s*\n?(.*?)\n?```"
307
+ matches = re.findall(pattern, response, re.DOTALL)
308
+
309
+ for match in matches:
310
+ try:
311
+ data = json.loads(match.strip())
312
+ if "tool" in data:
313
+ tool_calls.append(ToolCall(
314
+ name=data["tool"],
315
+ parameters=data.get("parameters", {}),
316
+ raw_text=match,
317
+ ))
318
+ except json.JSONDecodeError:
319
+ continue
320
+
321
+ # Pattern 2: JSON objects with "tool" key
322
+ json_pattern = r'\{[^{}]*"tool"\s*:\s*"[^"]+"\s*[^{}]*\}'
323
+ json_matches = re.findall(json_pattern, response)
324
+
325
+ for match in json_matches:
326
+ if match not in [tc.raw_text for tc in tool_calls]:
327
+ try:
328
+ data = json.loads(match)
329
+ if "tool" in data:
330
+ tool_calls.append(ToolCall(
331
+ name=data["tool"],
332
+ parameters=data.get("parameters", {}),
333
+ raw_text=match,
334
+ ))
335
+ except json.JSONDecodeError:
336
+ continue
337
+
338
+ return tool_calls
339
+
340
+ def _get_command_signature(self, tool_call: ToolCall) -> str:
341
+ """Get a signature for a tool call to detect repeated failures"""
342
+ import hashlib
343
+ sig_data = f"{tool_call.name}:{json.dumps(tool_call.parameters, sort_keys=True)}"
344
+ return hashlib.md5(sig_data.encode()).hexdigest()[:12]
345
+
346
+ def _check_loop_detection(self, tool_call: ToolCall) -> Optional[str]:
347
+ """Check if this command has failed too many times
348
+
349
+ Returns:
350
+ Warning message if loop detected, None otherwise
351
+ """
352
+ sig = self._get_command_signature(tool_call)
353
+ fail_count = self._failed_commands.get(sig, 0)
354
+
355
+ if fail_count >= self.config.max_same_error_retries:
356
+ return (
357
+ f"LOOP DETECTED: This command has failed {fail_count} times with the same error. "
358
+ f"You MUST try a DIFFERENT approach instead of repeating this command. "
359
+ f"Last error: {self._last_error}"
360
+ )
361
+ return None
362
+
363
+ def _record_failure(self, tool_call: ToolCall, error: str) -> None:
364
+ """Record a command failure for loop detection"""
365
+ sig = self._get_command_signature(tool_call)
366
+ self._failed_commands[sig] = self._failed_commands.get(sig, 0) + 1
367
+ self._last_error = error
368
+
369
+ def _execute_tool_call(self, tool_call: ToolCall) -> ToolResult:
370
+ """Execute a single tool call"""
371
+ tool = self.registry.get(tool_call.name)
372
+
373
+ if not tool:
374
+ return ToolResult(
375
+ success=False,
376
+ output="",
377
+ error=f"Unknown tool: {tool_call.name}",
378
+ tool_name=tool_call.name,
379
+ target=str(tool_call.parameters)[:30],
380
+ )
381
+
382
+ # Loop detection - check if this command has failed too many times
383
+ loop_warning = self._check_loop_detection(tool_call)
384
+ if loop_warning:
385
+ if HAS_CLI_UI:
386
+ warning(f"Loop detected for {tool_call.name}")
387
+ return ToolResult(
388
+ success=False,
389
+ output="",
390
+ error=loop_warning,
391
+ tool_name=tool_call.name,
392
+ target=tool._get_target(**tool_call.parameters),
393
+ )
394
+
395
+ # Check permission - special handling for Bash with safe commands
396
+ needs_approval = self.registry.needs_approval(tool_call.name)
397
+
398
+ # For Bash tool, check if command is safe (read-only)
399
+ if tool_call.name == "Bash" and "command" in tool_call.parameters:
400
+ from .tools.bash_tool import BashTool
401
+ if BashTool.is_safe_command(tool_call.parameters["command"]):
402
+ needs_approval = False
403
+
404
+ if needs_approval:
405
+ self.state = AgentState.WAITING_APPROVAL
406
+ approved = self._request_approval(tool_call)
407
+ if not approved:
408
+ return ToolResult(
409
+ success=False,
410
+ output="",
411
+ error="Tool execution denied by user",
412
+ tool_name=tool_call.name,
413
+ target=tool._get_target(**tool_call.parameters),
414
+ )
415
+
416
+ # Execute tool
417
+ self.state = AgentState.EXECUTING_TOOL
418
+
419
+ if HAS_CLI_UI:
420
+ target = tool._get_target(**tool_call.parameters)
421
+ log_action(tool_call.name, target, "running")
422
+
423
+ result = tool.run(**tool_call.parameters)
424
+
425
+ # Track failures for loop detection
426
+ if not result.success:
427
+ self._record_failure(tool_call, result.error or "Unknown error")
428
+
429
+ if HAS_CLI_UI:
430
+ state = "success" if result.success else "error"
431
+ if self.config.verbose or not result.success:
432
+ log_action(tool_call.name, result.target, state)
433
+ if self.config.show_tool_output and result.output:
434
+ # Show truncated output
435
+ output = result.output[:500]
436
+ if len(result.output) > 500:
437
+ output += "..."
438
+ print(f" {Color.DIM}{output}{Color.RESET}")
439
+
440
+ return result
441
+
442
+ def _request_approval(self, tool_call: ToolCall) -> bool:
443
+ """Request user approval for a tool call"""
444
+ tool = self.registry.get(tool_call.name)
445
+ target = tool._get_target(**tool_call.parameters) if tool else ""
446
+
447
+ print(f"\n{Color.YELLOW}⚠ Tool requires approval:{Color.RESET}")
448
+ print(f" {Color.BOLD}{tool_call.name}{Color.RESET}({Color.CYAN}{target}{Color.RESET})")
449
+
450
+ if tool_call.parameters:
451
+ print(f" Parameters: {json.dumps(tool_call.parameters, indent=2)[:200]}")
452
+
453
+ response = input(f"\n{Color.BOLD}Allow?{Color.RESET} [y/N/always]: ").strip().lower()
454
+
455
+ if response == "always":
456
+ self.registry.approve_for_session(tool_call.name)
457
+ return True
458
+ elif response in ["y", "yes"]:
459
+ return True
460
+ else:
461
+ return False
462
+
463
+ def _format_tool_results(self, results: List[ToolResult]) -> str:
464
+ """Format tool results for conversation"""
465
+ parts = []
466
+ for result in results:
467
+ if result.success:
468
+ parts.append(f"✓ {result.tool_name}({result.target}):\n{result.output}")
469
+ else:
470
+ parts.append(f"✗ {result.tool_name}({result.target}) failed: {result.error}")
471
+ return "\n\n".join(parts)
472
+
473
+ def _clean_response(self, response: str) -> str:
474
+ """Clean tool call markers from final response"""
475
+ # Remove tool blocks
476
+ response = re.sub(r"```tool\s*\n?.*?\n?```", "", response, flags=re.DOTALL)
477
+ # Remove JSON tool calls
478
+ response = re.sub(r'\{[^{}]*"tool"\s*:\s*"[^"]+"\s*[^{}]*\}', "", response)
479
+ return response.strip()
480
+
481
+ def get_tool_history(self) -> List[Dict[str, Any]]:
482
+ """Get history of tool calls and results"""
483
+ return [
484
+ {
485
+ "tool": r.tool_name,
486
+ "target": r.target,
487
+ "success": r.success,
488
+ "duration_ms": r.duration_ms,
489
+ }
490
+ for r in self.tool_results
491
+ ]
492
+
493
+
494
+ def create_agent(llm=None, **config_kwargs) -> Agent:
495
+ """Create an agent with default configuration
496
+
497
+ Args:
498
+ llm: LLM adapter
499
+ **config_kwargs: Configuration overrides
500
+
501
+ Returns:
502
+ Configured Agent instance
503
+ """
504
+ config = AgentConfig(**config_kwargs)
505
+ return Agent(llm=llm, config=config)