gns3-server/docs/gns3-copilot/todo/runtime-agent-params.md
YueGuobin 13a032ea2c chore: update author name and copyright headers
Updated the author name and copyright statements across the
gns3_copilot module. The name has been standardized from
"Guobin Yue" to "Yue Guobin (岳国宾)" to reflect the correct
author attribution including Chinese characters.
2026-03-09 11:46:28 +08:00

522 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Runtime Agent Parameters
## Overview
This document describes the design and implementation plan for adding runtime control parameters to the GNS3-Copilot agent. Currently, iteration limits and tool call constraints are hardcoded. This enhancement will allow users to pass temporary parameters at request time to control agent behavior.
## Problem Statement
### Current Limitations
1. **Hard-coded iteration limit**: The maximum number of LLM-tool iterations is fixed at 20 in `agent_service.py`
2. **No tool call limit**: There's no runtime control over the maximum number of tool calls per request
3. **Inflexible for complex tasks**: Long-running automation tasks may require more iterations than the default
4. **No cost control**: Users cannot limit the number of expensive tool calls (e.g., device configuration operations)
### User Impact
```
Scenario: User wants to configure OSPF on 10 routers
- Each router requires ~2-3 tool calls (check config, apply config, verify)
- Total: ~20-30 tool calls needed
- Current: No way to predict or control this
- Desired: User can set max_tool_calls=30 to ensure completion
```
## Current Architecture
### Parameter Flow
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 1. API Layer (chat.py) │
│ POST /v3/projects/{project_id}/chat/stream │
│ ChatRequest { message, session_id, temperature?, mode } │
└────────────────────────────┬────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ 2. Agent Service (agent_service.py) │
│ stream_chat(message, session_id, project_id, user_id, jwt, mode) │
│ → inputs = { │
│ "messages": [HumanMessage(...)], │
│ "llm_calls": 0, │
│ "remaining_steps": 20, ← HARDCODED │
│ "mode": mode │
│ } │
└────────────────────────────┬────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ 3. Agent Graph (gns3_copilot.py) │
│ recursion_limit_continue(state): │
│ if state["remaining_steps"] < 4: return END │
│ │
│ tool_node(state): │
│ → Execute tools without limit check │
└─────────────────────────────────────────────────────────────────────────┘
```
### Existing Controls
| Parameter | Location | Value | Description |
|-----------|----------|-------|-------------|
| `remaining_steps` | `agent_service.py:281` | 20 (hardcoded) | Total iteration count |
| Recursion threshold | `gns3_copilot.py` | `< 4` | Stop when remaining < 4 |
| `temperature` | `chat.py` | Reserved but not implemented | Runtime temperature override |
## Proposed Solution
### Option A: Simple Extension (Recommended)
**Scope**: API Schema + Agent Service modifications only
#### 1. API Schema Changes
**File**: `gns3server/schemas/controller/chat.py`
```python
class ChatRequest(BaseModel):
"""Chat request model."""
message: str = Field(..., description="User message content")
session_id: Optional[str] = Field(None, description="Session ID")
stream: bool = Field(default=True, description="Enable streaming response")
mode: Literal["text"] = Field(default="text", description="Interaction mode")
# New runtime control parameters
max_iterations: Optional[int] = Field(
None,
ge=1,
le=100,
description="Maximum number of LLM-tool iterations (default: 20). "
"Each iteration = LLM call + optional tool execution."
)
max_tool_calls: Optional[int] = Field(
None,
ge=1,
le=50,
description="Maximum number of tool calls per request (default: unlimited). "
"Useful for cost control and preventing runaway automation."
)
```
#### 2. Agent Service Changes
**File**: `gns3server/agent/gns3_copilot/agent_service.py`
```python
async def stream_chat(
self,
message: str,
session_id: str,
project_id: Optional[str] = None,
user_id: Optional[str] = None,
jwt_token: Optional[str] = None,
mode: str = "text",
llm_config: Optional[Dict[str, Any]] = None,
# New parameters
max_iterations: Optional[int] = None,
max_tool_calls: Optional[int] = None,
) -> AsyncGenerator[Dict[str, Any], None]:
"""
Stream chat responses from the agent.
Args:
message: User message
session_id: Session/thread ID for conversation continuity
project_id: GNS3 project ID (optional, for context)
user_id: User ID for metadata tracking
jwt_token: JWT token for API authentication (optional)
mode: Interaction mode (default: "text")
llm_config: LLM configuration dict (provider, model, api_key, etc.)
max_iterations: Maximum LLM-tool iterations (default: 20)
max_tool_calls: Maximum tool calls per request (default: unlimited)
Yields:
Dict containing SSE-compatible response chunks
"""
log.info(
"Stream chat started: project_id=%s, user_id=%s, session_id=%s, mode=%s, "
"max_iterations=%s, max_tool_calls=%s",
project_id,
user_id,
session_id,
mode,
max_iterations,
max_tool_calls,
)
# ... existing session setup code ...
# Build inputs with runtime parameters
inputs = {
"messages": [HumanMessage(content=message, id=str(uuid4()))],
"llm_calls": 0,
"remaining_steps": max_iterations or 20, # Use runtime parameter or default
"max_tool_calls": max_tool_calls or 999, # New: tool call limit
"tool_calls_count": 0, # New: counter
"mode": mode,
}
# ... rest of existing code ...
```
#### 3. Agent Graph Changes
**File**: `gns3server/agent/gns3_copilot/agent/gns3_copilot.py`
```python
def tool_node(state: dict, config: RunnableConfig | None = None):
"""
Performs the tool call with max_tool_calls limit.
Args:
state: Current agent state containing messages and tool_calls
config: Runnable configuration (optional)
Returns:
Dict with tool execution results or error message if limit exceeded
"""
tool_calls = state["messages"][-1].tool_calls
result = []
# Check tool call limit
max_tool_calls = state.get("max_tool_calls", 999)
current_tool_calls = state.get("tool_calls_count", 0)
if current_tool_calls + len(tool_calls) > max_tool_calls:
log.warning(
"Tool call limit exceeded: current=%d, requested=%d, max=%d",
current_tool_calls,
len(tool_calls),
max_tool_calls
)
# Return error message for each tool call
for tool_call in tool_calls:
result.append(
ToolMessage(
content=f"Tool call limit reached ({max_tool_calls} calls). "
f"Please simplify your request or break it into smaller steps. "
f"Current tool call count: {current_tool_calls}/{max_tool_calls}.",
tool_call_id=tool_call["id"],
name=tool_call["name"]
)
)
return {"messages": result}
# Execute tools normally
for tool_call in tool_calls:
tool_name = tool_call["name"]
tool = tools_by_name[tool_name]
try:
observation = tool.invoke(tool_call["args"])
except Exception as e:
log.error("Error executing tool %s: %s", tool_name, e)
observation = f"Error: {str(e)}"
result.append(
ToolMessage(
content=observation,
tool_call_id=tool_call["id"],
name=tool_call["name"]
)
)
# Update tool call counter
return {
"messages": result,
"tool_calls_count": current_tool_calls + len(tool_calls)
}
```
### State Management
The agent state needs to track the new fields:
```python
# Existing MessagesState already has:
# - messages: Annotated[List[BaseMessage], add_messages]
# - llm_calls: int
# - remaining_steps: int (from RemainingSteps)
# We add:
# - max_tool_calls: int (per-request limit)
# - tool_calls_count: int (running counter)
```
## Implementation Plan
| Step | Task | File(s) | Difficulty | Priority |
|------|------|---------|------------|----------|
| 1 | Extend `ChatRequest` schema | `schemas/controller/chat.py` | Low | P0 |
| 2 | Modify `stream_chat` signature | `agent_service.py` | Low | P0 |
| 3 | Use `max_iterations` in inputs | `agent_service.py` | Low | P0 |
| 4 | Implement `max_tool_calls` logic | `gns3_copilot.py` | ⭐⭐ Medium | P1 |
| 5 | Add tool call counter to state | `gns3_copilot.py` | Low | P1 |
| 6 | Update API documentation | `docs/` | Low | P1 |
| 7 | Add unit tests | `tests/` | ⭐⭐ Medium | P2 |
## Usage Examples
### Basic Usage
```bash
# Default behavior (no changes needed)
curl -X POST http://localhost:3080/v3/projects/{project_id}/chat/stream \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"message": "配置所有路由器的 OSPF"
}'
```
### With Custom Iteration Limit
```bash
# Allow more iterations for complex multi-device configuration
curl -X POST http://localhost:3080/v3/projects/{project_id}/chat/stream \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"message": "在10台路由器上配置OSPF、BGP和静态路由然后验证连通性",
"max_iterations": 50
}'
```
### With Tool Call Limit
```bash
# Limit tool calls for cost control
curl -X POST http://localhost:3080/v3/projects/{project_id}/chat/stream \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"message": "检查所有设备的接口状态",
"max_tool_calls": 15
}'
```
### Combined Parameters
```bash
# Complex task with both limits
curl -X POST http://localhost:3080/v3/projects/{project_id}/chat/stream \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"message": "配置整个实验室的网络并测试连通性",
"max_iterations": 40,
"max_tool_calls": 30
}'
```
## Security Considerations
### Parameter Limits
| Parameter | Min | Max | Default | Rationale |
|-----------|-----|-----|---------|-----------|
| `max_iterations` | 1 | 100 | 20 | Prevent infinite loops, allow complex tasks |
| `max_tool_calls` | 1 | 50 | unlimited (999) | Prevent tool abuse, control cost |
### Risk Mitigation
1. **Upper bounds enforced**: Pydantic validation prevents excessive values
2. **Graceful degradation**: Agent returns informative error messages when limits are reached
3. **Per-request scope**: Parameters don't persist across sessions
4. **Audit logging**: All parameters are logged for security analysis
### Edge Cases
```
Case 1: max_iterations = 1
→ Only one LLM call, no tool execution
→ Useful for simple Q&A without actions
Case 2: max_tool_calls = 1
→ Agent can only call one tool
→ Forces user to break complex tasks into smaller steps
Case 3: LLM ignores limits
→ Agent enforces limits at execution time
→ Returns error when limit exceeded
```
## Backward Compatibility
**Fully backward compatible**
- All new parameters are `Optional`
- Default values match current behavior
- Existing clients continue to work without changes
- No database migrations required
## Testing Strategy
### Unit Tests
```python
def test_max_iterations_enforced():
"""Test that agent respects max_iterations parameter"""
# Create request with max_iterations=5
# Verify agent stops after 5 iterations
def test_max_tool_calls_enforced():
"""Test that agent respects max_tool_calls parameter"""
# Create request with max_tool_calls=3
# Trigger 5 tool calls
# Verify only 3 execute, rest return error
def test_default_behavior_unchanged():
"""Test that omitting parameters uses defaults"""
# Create request without new parameters
# Verify behavior matches current implementation
```
### Integration Tests
```python
async def test_complex_multi_device_task():
"""Test complex task with increased limits"""
# Configure OSPF on 10 routers
# max_iterations=30, max_tool_calls=25
# Verify successful completion
async def test_tool_limit_error_message():
"""Test that limit errors are informative"""
# Set max_tool_calls=2
# Trigger 3 tool calls
# Verify third call returns helpful error message
```
## Future Enhancements
### Phase 2 Features
1. **Per-tool limits**:
```python
max_device_config_calls: Optional[int] = None
max_diagnostic_calls: Optional[int] = None
```
2. **Time-based limits**:
```python
max_execution_time_seconds: Optional[int] = None
```
3. **Cost estimation**:
```python
estimate_cost_before_execution: bool = False
```
4. **Adaptive limits**:
```python
auto_adjust_limits: bool = False # AI decides optimal limits
```
### Advanced Configuration
```python
class AdvancedAgentControls(BaseModel):
"""Advanced runtime controls for power users"""
# Execution limits
max_iterations: Optional[int] = None
max_tool_calls: Optional[int] = None
max_execution_time_seconds: Optional[int] = None
# Tool-specific limits
tool_limits: Dict[str, int] = Field(
default_factory=dict,
description="Per-tool call limits, e.g., {'execute_multiple_device_commands': 10}"
)
# Retry behavior
max_retries_per_tool: int = Field(default=1, ge=0, le=5)
retry_on_tool_error: bool = Field(default=False)
# Parallel execution
max_parallel_tools: int = Field(default=5, ge=1, le=20)
# Fallback behavior
on_limit_reached: Literal["fail", "warn", "continue"] = "warn"
```
## Related Documentation
- [AI Chat API Design](../ai-chat-api-design.md)
- [HITL Implementation Plan](./hitl-implementation-plan.md)
- [Tool Response Format Standard](./tool-response-format-standard.md)
## References
- LangGraph State Management: https://langchain-ai.github.io/langgraph/concepts/low_level/#state
- Pydantic Field Validation: https://docs.pydantic.dev/latest/concepts/fields/
- GNS3 Controller API: https://api.gns3.com/
## Discussion Points
### Open Questions
1. **Should limits be per-message or per-session?**
- Current: Per-message (per request)
- Alternative: Per-session (accumulate across conversation)
2. **Should we expose `remaining_steps` in the response?**
- Pro: User knows how many iterations left
- Con: Exposes internal implementation details
3. **Should we allow dynamic limit adjustment during execution?**
- Requires streaming parameter updates
- More complex but more flexible
4. **What about `temperature` override?**
- Already reserved in schema but not implemented
- Should we implement it in the same change?
### Decision Required
- [ ] Confirm parameter ranges (min/max values)
- [ ] Decide on error handling strategy (fail vs warn)
- [ ] Approve implementation plan
- [ ] Set target release version
---
**Status**: Design Draft - Ready for Review
**Author**: GNS3 Copilot Team
**Last Updated**: 2025-03-06
**Target Version**: TBD
---
## License
**Copyright © 2025 Yue Guobin (岳国宾)**
This work is licensed under the [Creative Commons Attribution-ShareAlike 4.0
International License (CC BY-SA 4.0)](https://creativecommons.org/licenses/by-sa/4.0/).
![CC BY-SA 4.0](https://i.creativecommons.org/l/by-sa/4.0/88x31.png)
### Summary
You are free to:
- **Share** Copy and redistribute the material in any medium or format
- **Adapt** Remix, transform, and build upon the material for any purpose
Under the following terms:
- **Attribution** You must give appropriate credit to **Yue Guobin (岳国宾)**, provide
a link to the license, and indicate if changes were made.
- **ShareAlike** If you remix, transform, or build upon the material, you must
distribute your contributions under the **same license** (CC BY-SA 4.0).
Full license text: [DESIGN_DOCS_LICENSE](../DESIGN_DOCS_LICENSE.md)