Graham Paasch commited on
Commit
fc9ae06
·
1 Parent(s): 5fbc4a8

Add LLM integration foundation for smart pipeline

Browse files

- Created agent/llm_client.py - unified client for OpenAI/Anthropic/OpenRouter
- Created agent/consultation.py - interactive multi-turn consultation
- Added PIPELINE_ENHANCEMENT.md with implementation roadmap
- Graceful fallback to mock responses if no API keys
- Ready for hackathon API credits integration

Next steps:
- Add OPENROUTER_API_KEY to .env
- Wire consultation into pipeline
- Enhance SoT generation with real network design
- Add hardware pricing database
- Implement streaming progress updates

Files changed (3) hide show
  1. PIPELINE_ENHANCEMENT.md +285 -0
  2. agent/consultation.py +180 -0
  3. agent/llm_client.py +276 -0
PIPELINE_ENHANCEMENT.md ADDED
@@ -0,0 +1,285 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Overgrowth Pipeline Enhancement Plan
2
+
3
+ ## What's Missing (Your Feedback Summary)
4
+
5
+ 1. **❌ No Interactive Consultation** - Should ask follow-up questions
6
+ 2. **❌ Source of Truth Incomplete** - No subnets, VLANs, real design
7
+ 3. **❌ BOM Pricing Wrong** - Shows $0 for everything
8
+ 4. **❌ Setup Guide Generic** - Missing firmware updates, real steps
9
+ 5. **❌ No Progress Visibility** - Can't see what AI agents are doing
10
+ 6. **❌ No Network Simulation Link** - Should show GNS3 topology
11
+
12
+ ## Implementation Plan
13
+
14
+ ### Phase 1: LLM Integration (PRIORITY)
15
+
16
+ **Files Created:**
17
+ - `agent/llm_client.py` - Unified LLM client (OpenAI/Anthropic/OpenRouter)
18
+ - `agent/consultation.py` - Interactive multi-turn consultation
19
+
20
+ **Setup Required:**
21
+
22
+ ```bash
23
+ # Add to .env file:
24
+ OPENROUTER_API_KEY=sk-or-v1-xxxxx # From your hackathon credits
25
+ # OR
26
+ OPENAI_API_KEY=sk-xxxxx
27
+ # OR
28
+ ANTHROPIC_API_KEY=sk-ant-xxxxx
29
+ ```
30
+
31
+ **Testing:**
32
+ ```python
33
+ from agent.consultation import NetworkConsultant
34
+
35
+ consultant = NetworkConsultant()
36
+ is_complete, output, intent = consultant.start_consultation(
37
+ "We're a coffee shop chain with 3 locations..."
38
+ )
39
+ print(output) # Will show follow-up questions
40
+
41
+ # User answers questions
42
+ is_complete, output, intent = consultant.continue_consultation(
43
+ "Budget is $50k, need it in 3 months, prefer Ubiquiti gear"
44
+ )
45
+ ```
46
+
47
+ ### Phase 2: Smart Network Design
48
+
49
+ **Enhance `stage2_generate_sot()` in pipeline_engine.py:**
50
+
51
+ ```python
52
+ def stage2_generate_sot(self, intent: NetworkIntent) -> NetworkModel:
53
+ """Use LLM to design actual network architecture"""
54
+
55
+ prompt = f"""
56
+ Design a production-ready network for:
57
+ {intent.description}
58
+
59
+ Requirements:
60
+ - Budget: {intent.budget}
61
+ - Locations: {intent.locations}
62
+ - Compliance: {intent.compliance_requirements}
63
+
64
+ Generate:
65
+ 1. VLAN scheme (management, data, voice, guest, security cameras, POS)
66
+ 2. IP subnetting plan (RFC1918 private addressing)
67
+ 3. Device list (switches, APs, routers, firewalls)
68
+ 4. Routing protocol (static, OSPF, BGP)
69
+ 5. Security policies
70
+
71
+ Return as structured JSON.
72
+ """
73
+
74
+ # Call LLM to generate real design
75
+ design = llm.chat([LLMMessage(role="user", content=prompt)])
76
+
77
+ # Parse into NetworkModel
78
+ return self._parse_network_design(design)
79
+ ```
80
+
81
+ **Example Output:**
82
+ ```yaml
83
+ vlans:
84
+ - id: 10
85
+ name: Management
86
+ subnet: 10.0.10.0/24
87
+ - id: 20
88
+ name: Guest_WiFi
89
+ subnet: 10.0.20.0/24
90
+ - id: 30
91
+ name: POS_Systems
92
+ subnet: 10.0.30.0/24
93
+ - id: 40
94
+ name: Security_Cameras
95
+ subnet: 10.0.40.0/24
96
+
97
+ devices:
98
+ - name: HQ-Core-SW01
99
+ role: core
100
+ model: Ubiquiti USW-Enterprise-48-PoE
101
+ mgmt_ip: 10.0.10.10
102
+ interfaces:
103
+ - name: eth0/1
104
+ vlan: 10
105
+ mode: access
106
+ ```
107
+
108
+ ### Phase 3: Real BOM Pricing
109
+
110
+ **Create `agent/hardware_pricing.py`:**
111
+
112
+ ```python
113
+ # Hardware database with real prices
114
+ HARDWARE_DB = {
115
+ "Ubiquiti USW-Enterprise-48-PoE": {
116
+ "price": 1799.00,
117
+ "category": "switch",
118
+ "vendor": "Ubiquiti"
119
+ },
120
+ "Ubiquiti U6-Enterprise": {
121
+ "price": 379.00,
122
+ "category": "access_point"
123
+ },
124
+ # ... more devices
125
+ }
126
+
127
+ def calculate_bom_cost(devices: List[Device]) -> float:
128
+ total = 0
129
+ for device in devices:
130
+ if device.model in HARDWARE_DB:
131
+ total += HARDWARE_DB[device.model]["price"]
132
+ return total
133
+ ```
134
+
135
+ ### Phase 4: Streaming Progress Updates
136
+
137
+ **Modify `app.py` to use Gradio streaming:**
138
+
139
+ ```python
140
+ def run_pipeline_streaming(user_input):
141
+ """Stream progress updates to UI"""
142
+ pipeline = OvergrowthPipeline()
143
+
144
+ # Stage 1: Consultation
145
+ yield "🤝 Stage 1: Starting consultation...\n"
146
+ consultant = NetworkConsultant()
147
+ is_complete, output, intent = consultant.start_consultation(user_input)
148
+
149
+ if not is_complete:
150
+ yield f"❓ **Follow-up questions:**\n{output}\n\n"
151
+ # Wait for user response (need UI update for this)
152
+ return
153
+
154
+ yield f"✅ Stage 1 Complete\n{output}\n\n"
155
+
156
+ # Stage 2: Generate SoT
157
+ yield "📋 Stage 2: Designing network architecture...\n"
158
+ model = pipeline.stage2_generate_sot(intent)
159
+ yield f"✅ Stage 2 Complete - {len(model.devices)} devices, {len(model.vlans)} VLANs\n\n"
160
+
161
+ # Stage 3: Diagrams
162
+ yield "📊 Stage 3: Generating topology diagrams...\n"
163
+ diagrams = pipeline.stage3_generate_diagrams(model)
164
+ yield f"✅ Stage 3 Complete\n\n"
165
+
166
+ # Continue with other stages...
167
+ ```
168
+
169
+ **Update Gradio interface:**
170
+ ```python
171
+ run_pipeline_btn.click(
172
+ fn=run_pipeline_streaming,
173
+ inputs=[pipeline_input],
174
+ outputs=[pipeline_status], # Single streaming output
175
+ show_progress=True
176
+ )
177
+ ```
178
+
179
+ ### Phase 5: GNS3 Simulation Integration
180
+
181
+ **Add to pipeline results:**
182
+
183
+ ```python
184
+ def stage6_autonomous_deploy(self, model: NetworkModel) -> Dict:
185
+ """Deploy to GNS3 lab"""
186
+ from agent.local_mcp import call_tool
187
+
188
+ # Build topology in GNS3
189
+ result = call_tool("create_project", {
190
+ "name": model.name,
191
+ "auto_start": True
192
+ })
193
+
194
+ project_id = result['project_id']
195
+
196
+ # Add devices
197
+ for device in model.devices:
198
+ call_tool("add_node", {
199
+ "project_id": project_id,
200
+ "name": device.name,
201
+ "node_type": device.role,
202
+ "x": ..., # Calculate layout
203
+ "y": ...
204
+ })
205
+
206
+ # Return simulation URL
207
+ return {
208
+ "success": True,
209
+ "gns3_url": f"http://lab.grahampaasch.com:3080/#/projects/{project_id}",
210
+ "topology_link": f"View live simulation: {gns3_url}"
211
+ }
212
+ ```
213
+
214
+ **Display in UI:**
215
+ ```markdown
216
+ ## 🌐 Live Network Simulation
217
+
218
+ Your network is being built in GNS3:
219
+ - **Project:** {model.name}
220
+ - **Devices:** {len(model.devices)} nodes
221
+ - **Status:** Deploying...
222
+
223
+ [View in GNS3](http://lab.grahampaasch.com:3080/#/projects/{project_id})
224
+ ```
225
+
226
+ ### Phase 6: Setup Guide with Real Steps
227
+
228
+ **Enhance `stage5_setup_guide()`:**
229
+
230
+ ```python
231
+ def generate_setup_guide(self, model: NetworkModel) -> SetupGuide:
232
+ """Generate detailed deployment guide"""
233
+
234
+ phases = [
235
+ {
236
+ "name": "Pre-Deployment Validation",
237
+ "duration": "1 hour",
238
+ "steps": [
239
+ "Verify all equipment received matches BOM",
240
+ "Check firmware versions - minimum required:",
241
+ *[f" - {d.model}: firmware v{get_min_firmware(d.model)}"
242
+ for d in model.devices],
243
+ "Unbox and inventory all equipment",
244
+ "Download latest firmware if upgrades needed"
245
+ ]
246
+ },
247
+ {
248
+ "name": "Firmware Updates",
249
+ "duration": "2-4 hours",
250
+ "steps": [
251
+ "Backup factory configs",
252
+ "Update devices one at a time",
253
+ *[f"Update {d.name} to {get_latest_firmware(d.model)}"
254
+ for d in model.devices],
255
+ "Verify boot-up and basic connectivity",
256
+ "Document firmware versions"
257
+ ]
258
+ },
259
+ # ... more realistic phases
260
+ ]
261
+
262
+ return SetupGuide(
263
+ network_name=model.name,
264
+ phases=phases,
265
+ # ... other details
266
+ )
267
+ ```
268
+
269
+ ## Next Steps
270
+
271
+ 1. **Add LLM API key to .env**
272
+ 2. **Test consultation flow**
273
+ 3. **Enhance pipeline stages with LLM calls**
274
+ 4. **Add hardware pricing database**
275
+ 5. **Implement streaming UI updates**
276
+ 6. **Wire up GNS3 deployment**
277
+
278
+ ## Cost Estimate
279
+
280
+ Using OpenRouter with Claude 3.5 Sonnet:
281
+ - Consultation: ~$0.02 per session
282
+ - Network Design: ~$0.05 per design
283
+ - Total per pipeline run: ~$0.10
284
+
285
+ Your hackathon credits should cover hundreds of runs.
agent/consultation.py ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Interactive Network Consultation
3
+ Multi-turn conversation to gather complete requirements
4
+ """
5
+
6
+ import json
7
+ import logging
8
+ from typing import Dict, List, Tuple, Optional
9
+ from agent.llm_client import LLMClient, LLMMessage
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ CONSULTATION_SYSTEM_PROMPT = """You are an expert network consultant helping a client design their network infrastructure.
15
+
16
+ Your job is to:
17
+ 1. Understand their business needs and technical requirements
18
+ 2. Ask clarifying questions to fill in gaps
19
+ 3. Probe for important details they may have forgotten (security, compliance, scalability, budget)
20
+ 4. Extract structured information: devices needed, VLANs, subnets, bandwidth, redundancy needs
21
+
22
+ When you have enough information, respond with JSON in this format:
23
+ ```json
24
+ {
25
+ "consultation_complete": true,
26
+ "network_intent": {
27
+ "description": "full description",
28
+ "locations": [...],
29
+ "business_requirements": [...],
30
+ "constraints": [...],
31
+ "timeline": "...",
32
+ "budget": "...",
33
+ "vendor_preference": "...",
34
+ "compliance_requirements": [...],
35
+ "bandwidth_requirements": {...},
36
+ "redundancy_requirements": {...}
37
+ }
38
+ }
39
+ ```
40
+
41
+ If you need more information, respond with:
42
+ ```json
43
+ {
44
+ "consultation_complete": false,
45
+ "questions": ["question 1", "question 2", ...],
46
+ "summary_so_far": "what we know so far"
47
+ }
48
+ ```
49
+
50
+ Be professional, thorough, and ask smart questions that a real network consultant would ask.
51
+ """
52
+
53
+
54
+ class NetworkConsultant:
55
+ """
56
+ Interactive consultation agent
57
+ Gathers complete requirements through conversation
58
+ """
59
+
60
+ def __init__(self):
61
+ self.llm = LLMClient()
62
+ self.conversation_history: List[LLMMessage] = []
63
+ self.intent_data: Optional[Dict] = None
64
+
65
+ def start_consultation(self, initial_description: str) -> Tuple[bool, str, Optional[Dict]]:
66
+ """
67
+ Start consultation process
68
+ Returns: (is_complete, next_questions_or_summary, intent_dict)
69
+ """
70
+ # Initialize conversation
71
+ self.conversation_history = [
72
+ LLMMessage(role="system", content=CONSULTATION_SYSTEM_PROMPT),
73
+ LLMMessage(role="user", content=f"Initial request: {initial_description}")
74
+ ]
75
+
76
+ # Get LLM response
77
+ response = self.llm.chat(self.conversation_history, temperature=0.3)
78
+
79
+ # Parse response
80
+ try:
81
+ # Extract JSON from response
82
+ json_start = response.find('{')
83
+ json_end = response.rfind('}') + 1
84
+ if json_start >= 0 and json_end > json_start:
85
+ json_str = response[json_start:json_end]
86
+ result = json.loads(json_str)
87
+
88
+ if result.get("consultation_complete"):
89
+ self.intent_data = result.get("network_intent")
90
+ summary = self._format_intent_summary(self.intent_data)
91
+ return True, summary, self.intent_data
92
+ else:
93
+ questions_text = self._format_questions(result.get("questions", []))
94
+ return False, questions_text, None
95
+ else:
96
+ # Fallback if no JSON
97
+ return False, response, None
98
+
99
+ except json.JSONDecodeError as e:
100
+ logger.error(f"Failed to parse LLM response as JSON: {e}")
101
+ return False, response, None
102
+
103
+ def continue_consultation(self, user_response: str) -> Tuple[bool, str, Optional[Dict]]:
104
+ """
105
+ Continue multi-turn consultation
106
+ Returns: (is_complete, next_questions_or_summary, intent_dict)
107
+ """
108
+ # Add user response to history
109
+ self.conversation_history.append(
110
+ LLMMessage(role="user", content=user_response)
111
+ )
112
+
113
+ # Get LLM response
114
+ response = self.llm.chat(self.conversation_history, temperature=0.3)
115
+ self.conversation_history.append(
116
+ LLMMessage(role="assistant", content=response)
117
+ )
118
+
119
+ # Parse response
120
+ try:
121
+ json_start = response.find('{')
122
+ json_end = response.rfind('}') + 1
123
+ if json_start >= 0 and json_end > json_start:
124
+ json_str = response[json_start:json_end]
125
+ result = json.loads(json_str)
126
+
127
+ if result.get("consultation_complete"):
128
+ self.intent_data = result.get("network_intent")
129
+ summary = self._format_intent_summary(self.intent_data)
130
+ return True, summary, self.intent_data
131
+ else:
132
+ questions_text = self._format_questions(result.get("questions", []))
133
+ summary = result.get("summary_so_far", "")
134
+ output = f"**Progress Summary:**\n{summary}\n\n**Additional Questions:**\n{questions_text}"
135
+ return False, output, None
136
+ else:
137
+ return False, response, None
138
+
139
+ except json.JSONDecodeError as e:
140
+ logger.error(f"Failed to parse LLM response: {e}")
141
+ return False, response, None
142
+
143
+ def _format_questions(self, questions: List[str]) -> str:
144
+ """Format questions as numbered list"""
145
+ return "\n".join(f"{i+1}. {q}" for i, q in enumerate(questions))
146
+
147
+ def _format_intent_summary(self, intent: Dict) -> str:
148
+ """Format final intent as readable summary"""
149
+ lines = ["## 📋 Consultation Complete!\n"]
150
+
151
+ lines.append(f"**Description:** {intent.get('description', 'N/A')}\n")
152
+
153
+ if intent.get('locations'):
154
+ lines.append(f"**Locations:** {len(intent['locations'])} sites")
155
+ for loc in intent['locations']:
156
+ lines.append(f" - {loc}")
157
+ lines.append("")
158
+
159
+ if intent.get('business_requirements'):
160
+ lines.append("**Business Requirements:**")
161
+ for req in intent['business_requirements']:
162
+ lines.append(f" - {req}")
163
+ lines.append("")
164
+
165
+ if intent.get('budget'):
166
+ lines.append(f"**Budget:** {intent['budget']}\n")
167
+
168
+ if intent.get('timeline'):
169
+ lines.append(f"**Timeline:** {intent['timeline']}\n")
170
+
171
+ if intent.get('vendor_preference'):
172
+ lines.append(f"**Preferred Vendors:** {intent['vendor_preference']}\n")
173
+
174
+ if intent.get('compliance_requirements'):
175
+ lines.append("**Compliance:**")
176
+ for req in intent['compliance_requirements']:
177
+ lines.append(f" - {req}")
178
+ lines.append("")
179
+
180
+ return "\n".join(lines)
agent/llm_client.py ADDED
@@ -0,0 +1,276 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ LLM Client for Overgrowth Pipeline
3
+ Supports multiple providers: OpenAI, Anthropic, OpenRouter
4
+ """
5
+
6
+ import os
7
+ import json
8
+ import logging
9
+ from typing import Dict, List, Optional, Iterator, Any
10
+ from dataclasses import dataclass
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ @dataclass
16
+ class LLMMessage:
17
+ role: str # system, user, assistant
18
+ content: str
19
+
20
+
21
+ class LLMClient:
22
+ """
23
+ Unified LLM client supporting multiple providers
24
+ Falls back gracefully if API keys not available
25
+ """
26
+
27
+ def __init__(self):
28
+ self.openai_key = os.getenv("OPENAI_API_KEY")
29
+ self.anthropic_key = os.getenv("ANTHROPIC_API_KEY")
30
+ self.openrouter_key = os.getenv("OPENROUTER_API_KEY")
31
+
32
+ # Determine which provider to use
33
+ self.provider = self._detect_provider()
34
+
35
+ if self.provider:
36
+ logger.info(f"LLM client initialized with provider: {self.provider}")
37
+ else:
38
+ logger.warning("No LLM API keys found - using mock responses")
39
+
40
+ def _detect_provider(self) -> Optional[str]:
41
+ """Detect which LLM provider is available"""
42
+ if self.openrouter_key:
43
+ return "openrouter"
44
+ elif self.openai_key:
45
+ return "openai"
46
+ elif self.anthropic_key:
47
+ return "anthropic"
48
+ return None
49
+
50
+ def chat(
51
+ self,
52
+ messages: List[LLMMessage],
53
+ temperature: float = 0.7,
54
+ max_tokens: int = 4000,
55
+ stream: bool = False
56
+ ) -> str:
57
+ """
58
+ Send chat completion request
59
+ Returns response text or yields chunks if streaming
60
+ """
61
+ if not self.provider:
62
+ return self._mock_response(messages)
63
+
64
+ if self.provider == "openrouter":
65
+ return self._call_openrouter(messages, temperature, max_tokens, stream)
66
+ elif self.provider == "openai":
67
+ return self._call_openai(messages, temperature, max_tokens, stream)
68
+ elif self.provider == "anthropic":
69
+ return self._call_anthropic(messages, temperature, max_tokens, stream)
70
+
71
+ def chat_stream(
72
+ self,
73
+ messages: List[LLMMessage],
74
+ temperature: float = 0.7,
75
+ max_tokens: int = 4000
76
+ ) -> Iterator[str]:
77
+ """Stream chat completion response"""
78
+ if not self.provider:
79
+ yield self._mock_response(messages)
80
+ return
81
+
82
+ if self.provider == "openrouter":
83
+ yield from self._stream_openrouter(messages, temperature, max_tokens)
84
+ elif self.provider == "openai":
85
+ yield from self._stream_openai(messages, temperature, max_tokens)
86
+ elif self.provider == "anthropic":
87
+ yield from self._stream_anthropic(messages, temperature, max_tokens)
88
+
89
+ def _call_openrouter(self, messages, temperature, max_tokens, stream):
90
+ """Call OpenRouter API"""
91
+ try:
92
+ import requests
93
+
94
+ url = "https://openrouter.ai/api/v1/chat/completions"
95
+ headers = {
96
+ "Authorization": f"Bearer {self.openrouter_key}",
97
+ "Content-Type": "application/json"
98
+ }
99
+
100
+ data = {
101
+ "model": "anthropic/claude-3.5-sonnet", # Good balance of cost/quality
102
+ "messages": [{"role": m.role, "content": m.content} for m in messages],
103
+ "temperature": temperature,
104
+ "max_tokens": max_tokens
105
+ }
106
+
107
+ response = requests.post(url, headers=headers, json=data, timeout=60)
108
+ response.raise_for_status()
109
+
110
+ return response.json()['choices'][0]['message']['content']
111
+
112
+ except Exception as e:
113
+ logger.error(f"OpenRouter API error: {e}")
114
+ return self._mock_response(messages)
115
+
116
+ def _stream_openrouter(self, messages, temperature, max_tokens):
117
+ """Stream from OpenRouter"""
118
+ try:
119
+ import requests
120
+
121
+ url = "https://openrouter.ai/api/v1/chat/completions"
122
+ headers = {
123
+ "Authorization": f"Bearer {self.openrouter_key}",
124
+ "Content-Type": "application/json"
125
+ }
126
+
127
+ data = {
128
+ "model": "anthropic/claude-3.5-sonnet",
129
+ "messages": [{"role": m.role, "content": m.content} for m in messages],
130
+ "temperature": temperature,
131
+ "max_tokens": max_tokens,
132
+ "stream": True
133
+ }
134
+
135
+ with requests.post(url, headers=headers, json=data, stream=True, timeout=60) as response:
136
+ response.raise_for_status()
137
+ for line in response.iter_lines():
138
+ if line:
139
+ line = line.decode('utf-8')
140
+ if line.startswith('data: '):
141
+ line = line[6:]
142
+ if line == '[DONE]':
143
+ break
144
+ try:
145
+ chunk = json.loads(line)
146
+ if 'choices' in chunk and len(chunk['choices']) > 0:
147
+ delta = chunk['choices'][0].get('delta', {})
148
+ if 'content' in delta:
149
+ yield delta['content']
150
+ except json.JSONDecodeError:
151
+ continue
152
+
153
+ except Exception as e:
154
+ logger.error(f"OpenRouter streaming error: {e}")
155
+ yield self._mock_response(messages)
156
+
157
+ def _call_openai(self, messages, temperature, max_tokens, stream):
158
+ """Call OpenAI API"""
159
+ try:
160
+ from openai import OpenAI
161
+ client = OpenAI(api_key=self.openai_key)
162
+
163
+ response = client.chat.completions.create(
164
+ model="gpt-4o",
165
+ messages=[{"role": m.role, "content": m.content} for m in messages],
166
+ temperature=temperature,
167
+ max_tokens=max_tokens
168
+ )
169
+
170
+ return response.choices[0].message.content
171
+
172
+ except Exception as e:
173
+ logger.error(f"OpenAI API error: {e}")
174
+ return self._mock_response(messages)
175
+
176
+ def _stream_openai(self, messages, temperature, max_tokens):
177
+ """Stream from OpenAI"""
178
+ try:
179
+ from openai import OpenAI
180
+ client = OpenAI(api_key=self.openai_key)
181
+
182
+ stream = client.chat.completions.create(
183
+ model="gpt-4o",
184
+ messages=[{"role": m.role, "content": m.content} for m in messages],
185
+ temperature=temperature,
186
+ max_tokens=max_tokens,
187
+ stream=True
188
+ )
189
+
190
+ for chunk in stream:
191
+ if chunk.choices[0].delta.content:
192
+ yield chunk.choices[0].delta.content
193
+
194
+ except Exception as e:
195
+ logger.error(f"OpenAI streaming error: {e}")
196
+ yield self._mock_response(messages)
197
+
198
+ def _call_anthropic(self, messages, temperature, max_tokens, stream):
199
+ """Call Anthropic API"""
200
+ try:
201
+ import anthropic
202
+ client = anthropic.Anthropic(api_key=self.anthropic_key)
203
+
204
+ # Convert messages format
205
+ system_msg = None
206
+ user_messages = []
207
+ for m in messages:
208
+ if m.role == "system":
209
+ system_msg = m.content
210
+ else:
211
+ user_messages.append({"role": m.role, "content": m.content})
212
+
213
+ response = client.messages.create(
214
+ model="claude-3-5-sonnet-20241022",
215
+ max_tokens=max_tokens,
216
+ temperature=temperature,
217
+ system=system_msg if system_msg else "You are a helpful network automation assistant.",
218
+ messages=user_messages
219
+ )
220
+
221
+ return response.content[0].text
222
+
223
+ except Exception as e:
224
+ logger.error(f"Anthropic API error: {e}")
225
+ return self._mock_response(messages)
226
+
227
+ def _stream_anthropic(self, messages, temperature, max_tokens):
228
+ """Stream from Anthropic"""
229
+ try:
230
+ import anthropic
231
+ client = anthropic.Anthropic(api_key=self.anthropic_key)
232
+
233
+ # Convert messages format
234
+ system_msg = None
235
+ user_messages = []
236
+ for m in messages:
237
+ if m.role == "system":
238
+ system_msg = m.content
239
+ else:
240
+ user_messages.append({"role": m.role, "content": m.content})
241
+
242
+ with client.messages.stream(
243
+ model="claude-3-5-sonnet-20241022",
244
+ max_tokens=max_tokens,
245
+ temperature=temperature,
246
+ system=system_msg if system_msg else "You are a helpful network automation assistant.",
247
+ messages=user_messages
248
+ ) as stream:
249
+ for text in stream.text_stream:
250
+ yield text
251
+
252
+ except Exception as e:
253
+ logger.error(f"Anthropic streaming error: {e}")
254
+ yield self._mock_response(messages)
255
+
256
+ def _mock_response(self, messages: List[LLMMessage]) -> str:
257
+ """Return mock response when no API key available"""
258
+ last_user_msg = next((m.content for m in reversed(messages) if m.role == "user"), "")
259
+
260
+ if "consultation" in last_user_msg.lower():
261
+ return json.dumps({
262
+ "questions": [
263
+ "What is your total budget for this network deployment?",
264
+ "Do you have any vendor preferences? (Cisco, Juniper, Arista, Ubiquiti, MikroTik)",
265
+ "When do you need this network operational?",
266
+ "Do you have existing infrastructure to integrate with?",
267
+ "What are your bandwidth requirements per location?"
268
+ ],
269
+ "clarifications": [
270
+ "How many total concurrent devices across all 3 locations?",
271
+ "Do you need site-to-site VPN between locations?",
272
+ "PCI-DSS compliance required for payment processing?"
273
+ ]
274
+ })
275
+
276
+ return "Mock LLM response - please configure API keys"