DD009 commited on
Commit
4af38ee
Β·
verified Β·
1 Parent(s): dce0034

Upload main.py

Browse files
Files changed (1) hide show
  1. src/main.py +845 -0
src/main.py ADDED
@@ -0,0 +1,845 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Developer Productivity Agent
3
+ RAG-based system using Pinecone for vector storage and GPT-4o-mini.
4
+
5
+ Features:
6
+ - Pinecone vector database (2GB free tier)
7
+ - Divided LLM Architecture for cost optimization
8
+ - Real-time cost tracking and analytics
9
+ - OpenAI embeddings (text-embedding-3-small)
10
+ """
11
+
12
+ import os
13
+ import json
14
+ import time
15
+ from pathlib import Path
16
+ from typing import List, Dict, Any, Optional
17
+ import hashlib
18
+ from datetime import datetime
19
+
20
+ # Core dependencies
21
+ from fastapi import FastAPI, HTTPException
22
+ from fastapi.middleware.cors import CORSMiddleware
23
+ from pydantic import BaseModel
24
+ import uvicorn
25
+
26
+ # Vector database - Pinecone
27
+ from pinecone import Pinecone, ServerlessSpec
28
+
29
+ # LLM client
30
+ from openai import OpenAI
31
+
32
+ # Code parsing
33
+ import ast
34
+ import re
35
+ from dataclasses import dataclass, field
36
+
37
+ # ============================================================================
38
+ # Configuration
39
+ # ============================================================================
40
+
41
+ class Config:
42
+ """Application configuration"""
43
+ # OpenAI
44
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
45
+
46
+ # Pinecone
47
+ PINECONE_API_KEY = os.getenv("PINECONE_API_KEY", "")
48
+ PINECONE_INDEX_NAME = os.getenv("PINECONE_INDEX_NAME", "codebase-index")
49
+ PINECONE_CLOUD = "aws"
50
+ PINECONE_REGION = "us-east-1"
51
+
52
+ # Models
53
+ ARCHITECT_MODEL = "gpt-4o-mini"
54
+ DEVELOPER_MODEL = "gpt-4o-mini"
55
+ EMBEDDING_MODEL = "text-embedding-3-small"
56
+ EMBEDDING_DIM = 1536
57
+
58
+ # Chunking
59
+ CHUNK_SIZE = 1500
60
+ CHUNK_OVERLAP = 200
61
+ TOP_K_RESULTS = 10
62
+
63
+ # Cost tracking (per 1M tokens)
64
+ COST_GPT4O_MINI_INPUT = 0.15 # $0.15 per 1M input tokens
65
+ COST_GPT4O_MINI_OUTPUT = 0.60 # $0.60 per 1M output tokens
66
+ COST_EMBEDDING = 0.02 # $0.02 per 1M tokens
67
+ COST_GPT4_INPUT = 30.0 # For comparison - traditional approach
68
+ COST_GPT4_OUTPUT = 60.0
69
+
70
+
71
+ # ============================================================================
72
+ # Cost Tracker
73
+ # ============================================================================
74
+
75
+ class CostTracker:
76
+ """Tracks API costs and calculates savings"""
77
+
78
+ def __init__(self):
79
+ self.reset()
80
+
81
+ def reset(self):
82
+ """Reset all counters"""
83
+ self.embedding_tokens = 0
84
+ self.architect_input_tokens = 0
85
+ self.architect_output_tokens = 0
86
+ self.developer_input_tokens = 0
87
+ self.developer_output_tokens = 0
88
+ self.api_calls = 0
89
+ self.tickets_processed = 0
90
+ self.questions_answered = 0
91
+ self.start_time = datetime.now()
92
+ self.history = []
93
+
94
+ def add_embedding(self, tokens: int):
95
+ """Track embedding tokens"""
96
+ self.embedding_tokens += tokens
97
+ self.api_calls += 1
98
+
99
+ def add_architect_call(self, input_tokens: int, output_tokens: int):
100
+ """Track architect LLM call"""
101
+ self.architect_input_tokens += input_tokens
102
+ self.architect_output_tokens += output_tokens
103
+ self.api_calls += 1
104
+
105
+ def add_developer_call(self, input_tokens: int, output_tokens: int):
106
+ """Track developer LLM call"""
107
+ self.developer_input_tokens += input_tokens
108
+ self.developer_output_tokens += output_tokens
109
+ self.api_calls += 1
110
+
111
+ def record_ticket(self):
112
+ """Record a processed ticket"""
113
+ self.tickets_processed += 1
114
+ self._add_to_history("ticket")
115
+
116
+ def record_question(self):
117
+ """Record an answered question"""
118
+ self.questions_answered += 1
119
+ self._add_to_history("question")
120
+
121
+ def _add_to_history(self, event_type: str):
122
+ """Add event to history"""
123
+ self.history.append({
124
+ "timestamp": datetime.now().isoformat(),
125
+ "type": event_type,
126
+ "cumulative_cost": self.get_actual_cost(),
127
+ "cumulative_savings": self.get_savings()
128
+ })
129
+
130
+ def get_actual_cost(self) -> float:
131
+ """Calculate actual cost with our approach"""
132
+ config = Config()
133
+
134
+ embedding_cost = (self.embedding_tokens / 1_000_000) * config.COST_EMBEDDING
135
+ architect_cost = (
136
+ (self.architect_input_tokens / 1_000_000) * config.COST_GPT4O_MINI_INPUT +
137
+ (self.architect_output_tokens / 1_000_000) * config.COST_GPT4O_MINI_OUTPUT
138
+ )
139
+ developer_cost = (
140
+ (self.developer_input_tokens / 1_000_000) * config.COST_GPT4O_MINI_INPUT +
141
+ (self.developer_output_tokens / 1_000_000) * config.COST_GPT4O_MINI_OUTPUT
142
+ )
143
+
144
+ return embedding_cost + architect_cost + developer_cost
145
+
146
+ def get_traditional_cost(self) -> float:
147
+ """Calculate what it would cost with traditional GPT-4 approach"""
148
+ config = Config()
149
+
150
+ # Traditional approach uses GPT-4 for everything
151
+ total_input = self.architect_input_tokens + self.developer_input_tokens
152
+ total_output = self.architect_output_tokens + self.developer_output_tokens
153
+
154
+ return (
155
+ (total_input / 1_000_000) * config.COST_GPT4_INPUT +
156
+ (total_output / 1_000_000) * config.COST_GPT4_OUTPUT
157
+ )
158
+
159
+ def get_savings(self) -> float:
160
+ """Calculate cost savings"""
161
+ return self.get_traditional_cost() - self.get_actual_cost()
162
+
163
+ def get_savings_percentage(self) -> float:
164
+ """Calculate savings as percentage"""
165
+ traditional = self.get_traditional_cost()
166
+ if traditional == 0:
167
+ return 0
168
+ return ((traditional - self.get_actual_cost()) / traditional) * 100
169
+
170
+ def get_stats(self) -> Dict[str, Any]:
171
+ """Get comprehensive statistics"""
172
+ return {
173
+ "actual_cost": round(self.get_actual_cost(), 6),
174
+ "traditional_cost": round(self.get_traditional_cost(), 6),
175
+ "savings": round(self.get_savings(), 6),
176
+ "savings_percentage": round(self.get_savings_percentage(), 2),
177
+ "total_tokens": {
178
+ "embedding": self.embedding_tokens,
179
+ "architect_input": self.architect_input_tokens,
180
+ "architect_output": self.architect_output_tokens,
181
+ "developer_input": self.developer_input_tokens,
182
+ "developer_output": self.developer_output_tokens,
183
+ "total": (self.embedding_tokens + self.architect_input_tokens +
184
+ self.architect_output_tokens + self.developer_input_tokens +
185
+ self.developer_output_tokens)
186
+ },
187
+ "api_calls": self.api_calls,
188
+ "tickets_processed": self.tickets_processed,
189
+ "questions_answered": self.questions_answered,
190
+ "session_duration_minutes": round((datetime.now() - self.start_time).seconds / 60, 2),
191
+ "cost_per_ticket": round(self.get_actual_cost() / max(self.tickets_processed, 1), 6),
192
+ "history": self.history[-50:] # Last 50 events
193
+ }
194
+
195
+
196
+ # Global cost tracker
197
+ cost_tracker = CostTracker()
198
+
199
+
200
+ # ============================================================================
201
+ # Data Models
202
+ # ============================================================================
203
+
204
+ class JiraTicket(BaseModel):
205
+ ticket_id: str
206
+ title: str
207
+ description: str
208
+ acceptance_criteria: Optional[str] = None
209
+ labels: Optional[List[str]] = None
210
+
211
+ class ImplementationPlan(BaseModel):
212
+ ticket_summary: str
213
+ key_entities: List[str]
214
+ relevant_files: List[Dict[str, str]]
215
+ implementation_steps: List[str]
216
+ prerequisites: List[str]
217
+ boilerplate_code: Dict[str, str]
218
+ architecture_notes: str
219
+ estimated_complexity: str
220
+
221
+
222
+ # ============================================================================
223
+ # Pinecone-based Codebase Indexer
224
+ # ============================================================================
225
+
226
+ class CodebaseIndexer:
227
+ """Indexes codebase into Pinecone vector database"""
228
+
229
+ def __init__(self, config: Config):
230
+ self.config = config
231
+ self._openai_client = None
232
+ self._pinecone_client = None
233
+ self._index = None
234
+
235
+ @property
236
+ def openai_client(self):
237
+ if self._openai_client is None:
238
+ if not self.config.OPENAI_API_KEY:
239
+ raise ValueError("OpenAI API key required")
240
+ self._openai_client = OpenAI(api_key=self.config.OPENAI_API_KEY)
241
+ return self._openai_client
242
+
243
+ @property
244
+ def index(self):
245
+ if self._index is None:
246
+ if not self.config.PINECONE_API_KEY:
247
+ raise ValueError("Pinecone API key required")
248
+
249
+ # Initialize Pinecone
250
+ pc = Pinecone(api_key=self.config.PINECONE_API_KEY)
251
+
252
+ # Create index if not exists
253
+ if self.config.PINECONE_INDEX_NAME not in pc.list_indexes().names():
254
+ pc.create_index(
255
+ name=self.config.PINECONE_INDEX_NAME,
256
+ dimension=self.config.EMBEDDING_DIM,
257
+ metric="cosine",
258
+ spec=ServerlessSpec(
259
+ cloud=self.config.PINECONE_CLOUD,
260
+ region=self.config.PINECONE_REGION
261
+ )
262
+ )
263
+ # Wait for index to be ready
264
+ time.sleep(5)
265
+
266
+ self._index = pc.Index(self.config.PINECONE_INDEX_NAME)
267
+ print(f"πŸ“‚ Pinecone index ready: {self.config.PINECONE_INDEX_NAME}")
268
+
269
+ return self._index
270
+
271
+ def _get_embedding(self, text: str) -> List[float]:
272
+ """Get embedding and track cost"""
273
+ # Estimate tokens (rough: 1 token β‰ˆ 4 chars)
274
+ tokens = len(text) // 4
275
+ cost_tracker.add_embedding(tokens)
276
+
277
+ response = self.openai_client.embeddings.create(
278
+ model=self.config.EMBEDDING_MODEL,
279
+ input=text
280
+ )
281
+ return response.data[0].embedding
282
+
283
+ def _get_embeddings_batch(self, texts: List[str]) -> List[List[float]]:
284
+ """Batch embeddings with cost tracking"""
285
+ if not texts:
286
+ return []
287
+
288
+ tokens = sum(len(t) // 4 for t in texts)
289
+ cost_tracker.add_embedding(tokens)
290
+
291
+ response = self.openai_client.embeddings.create(
292
+ model=self.config.EMBEDDING_MODEL,
293
+ input=texts
294
+ )
295
+ return [item.embedding for item in response.data]
296
+
297
+ def _detect_language(self, file_path: str) -> str:
298
+ ext_map = {
299
+ '.py': 'python', '.js': 'javascript', '.jsx': 'javascript',
300
+ '.ts': 'typescript', '.tsx': 'typescript', '.java': 'java',
301
+ '.go': 'go', '.rs': 'rust', '.cpp': 'cpp', '.c': 'c',
302
+ }
303
+ return ext_map.get(Path(file_path).suffix.lower(), 'unknown')
304
+
305
+ def _chunk_content(self, content: str, file_path: str) -> List[Dict[str, Any]]:
306
+ """Chunk content with overlap"""
307
+ chunks = []
308
+ lines = content.split('\n')
309
+ chunk_lines = self.config.CHUNK_SIZE // 50
310
+ overlap_lines = self.config.CHUNK_OVERLAP // 50
311
+
312
+ i = 0
313
+ chunk_idx = 0
314
+ while i < len(lines):
315
+ end = min(i + chunk_lines, len(lines))
316
+ chunk_content = '\n'.join(lines[i:end])
317
+
318
+ if chunk_content.strip(): # Skip empty chunks
319
+ chunks.append({
320
+ 'content': chunk_content,
321
+ 'file_path': file_path,
322
+ 'chunk_index': chunk_idx,
323
+ 'line_start': i + 1,
324
+ 'line_end': end,
325
+ 'language': self._detect_language(file_path)
326
+ })
327
+
328
+ i = end - overlap_lines if end < len(lines) else end
329
+ chunk_idx += 1
330
+
331
+ return chunks
332
+
333
+ def index_file(self, file_path: str, content: str) -> int:
334
+ """Index a single file into Pinecone"""
335
+ chunks = self._chunk_content(content, file_path)
336
+
337
+ if not chunks:
338
+ return 0
339
+
340
+ # Get embeddings
341
+ texts = [c['content'] for c in chunks]
342
+ embeddings = self._get_embeddings_batch(texts)
343
+
344
+ # Prepare vectors for Pinecone
345
+ vectors = []
346
+ for i, chunk in enumerate(chunks):
347
+ vector_id = hashlib.md5(
348
+ f"{file_path}_{chunk['chunk_index']}".encode()
349
+ ).hexdigest()
350
+
351
+ vectors.append({
352
+ "id": vector_id,
353
+ "values": embeddings[i],
354
+ "metadata": {
355
+ "file_path": file_path,
356
+ "chunk_index": chunk['chunk_index'],
357
+ "language": chunk['language'],
358
+ "line_start": chunk['line_start'],
359
+ "line_end": chunk['line_end'],
360
+ "content": chunk['content'][:1000] # Pinecone metadata limit
361
+ }
362
+ })
363
+
364
+ # Upsert to Pinecone
365
+ self.index.upsert(vectors=vectors)
366
+
367
+ return len(chunks)
368
+
369
+ def index_directory(self, directory_path: str, extensions: List[str] = None) -> Dict[str, int]:
370
+ """Index all files in a directory"""
371
+ if extensions is None:
372
+ extensions = ['.py', '.js', '.jsx', '.ts', '.tsx', '.java', '.go']
373
+
374
+ results = {}
375
+ directory = Path(directory_path)
376
+
377
+ for ext in extensions:
378
+ for file_path in directory.rglob(f"*{ext}"):
379
+ if any(skip in str(file_path) for skip in ['node_modules', '__pycache__', '.git', 'venv']):
380
+ continue
381
+
382
+ try:
383
+ content = file_path.read_text(encoding='utf-8')
384
+ chunks = self.index_file(str(file_path), content)
385
+ results[str(file_path)] = chunks
386
+ print(f" βœ… {file_path.name}: {chunks} chunks")
387
+ except Exception as e:
388
+ results[str(file_path)] = f"Error: {e}"
389
+
390
+ return results
391
+
392
+ def search(self, query: str, top_k: int = None) -> List[Dict[str, Any]]:
393
+ """Search codebase"""
394
+ if top_k is None:
395
+ top_k = self.config.TOP_K_RESULTS
396
+
397
+ query_embedding = self._get_embedding(query)
398
+
399
+ results = self.index.query(
400
+ vector=query_embedding,
401
+ top_k=top_k,
402
+ include_metadata=True
403
+ )
404
+
405
+ formatted = []
406
+ for match in results.matches:
407
+ formatted.append({
408
+ 'content': match.metadata.get('content', ''),
409
+ 'metadata': {
410
+ 'file_path': match.metadata.get('file_path', ''),
411
+ 'line_start': match.metadata.get('line_start', 0),
412
+ 'line_end': match.metadata.get('line_end', 0),
413
+ 'language': match.metadata.get('language', '')
414
+ },
415
+ 'score': match.score
416
+ })
417
+
418
+ return formatted
419
+
420
+ def get_stats(self) -> Dict[str, Any]:
421
+ """Get index statistics"""
422
+ try:
423
+ stats = self.index.describe_index_stats()
424
+ return {
425
+ 'total_chunks': stats.total_vector_count,
426
+ 'index_name': self.config.PINECONE_INDEX_NAME,
427
+ 'dimension': stats.dimension
428
+ }
429
+ except:
430
+ return {'total_chunks': 0, 'index_name': self.config.PINECONE_INDEX_NAME}
431
+
432
+ def clear_index(self):
433
+ """Clear all vectors"""
434
+ try:
435
+ self.index.delete(delete_all=True)
436
+ print("⚠️ Index cleared!")
437
+ except:
438
+ pass
439
+
440
+
441
+ # ============================================================================
442
+ # LLM Specialists with Cost Tracking
443
+ # ============================================================================
444
+
445
+ class ArchitectLLM:
446
+ """LLM #1: Architect - planning and analysis"""
447
+
448
+ def __init__(self, config: Config):
449
+ self.config = config
450
+ self._client = None
451
+ self.model = config.ARCHITECT_MODEL
452
+
453
+ @property
454
+ def client(self):
455
+ if self._client is None:
456
+ if not self.config.OPENAI_API_KEY:
457
+ raise ValueError("OpenAI API key not set!")
458
+ self._client = OpenAI(api_key=self.config.OPENAI_API_KEY)
459
+ return self._client
460
+
461
+ def reset_client(self):
462
+ self._client = None
463
+
464
+ def analyze_ticket(self, ticket: JiraTicket) -> Dict[str, Any]:
465
+ prompt = f"""Analyze this Jira ticket for implementation:
466
+
467
+ ID: {ticket.ticket_id}
468
+ Title: {ticket.title}
469
+ Description: {ticket.description}
470
+ Acceptance Criteria: {ticket.acceptance_criteria or 'Not specified'}
471
+
472
+ Provide JSON:
473
+ {{
474
+ "summary": "2-3 sentence summary",
475
+ "key_entities": ["entity1", "entity2"],
476
+ "technical_keywords": ["keyword1", "keyword2"],
477
+ "prerequisites": ["prereq1"],
478
+ "complexity": "Low/Medium/High",
479
+ "complexity_reason": "why",
480
+ "risks": ["risk1"]
481
+ }}"""
482
+
483
+ response = self.client.chat.completions.create(
484
+ model=self.model,
485
+ messages=[{"role": "user", "content": prompt}],
486
+ temperature=0.3
487
+ )
488
+
489
+ # Track costs
490
+ usage = response.usage
491
+ cost_tracker.add_architect_call(usage.prompt_tokens, usage.completion_tokens)
492
+
493
+ content = response.choices[0].message.content
494
+ try:
495
+ content = re.sub(r'^```json?\s*', '', content.strip())
496
+ content = re.sub(r'\s*```$', '', content)
497
+ return json.loads(content)
498
+ except:
499
+ return {"summary": content, "key_entities": [], "technical_keywords": [],
500
+ "prerequisites": [], "complexity": "Unknown", "complexity_reason": "", "risks": []}
501
+
502
+ def create_implementation_strategy(self, ticket_analysis: Dict, code_context: List[Dict]) -> Dict:
503
+ context_str = "\n".join([
504
+ f"File: {c['metadata'].get('file_path', '?')}\n{c['content'][:500]}"
505
+ for c in code_context[:5]
506
+ ])
507
+
508
+ prompt = f"""Create a detailed implementation strategy for this ticket:
509
+
510
+ Ticket Analysis: {json.dumps(ticket_analysis, indent=2)}
511
+
512
+ Relevant Code Context:
513
+ {context_str}
514
+
515
+ Provide a comprehensive JSON response with:
516
+ {{
517
+ "architecture_notes": "Detailed explanation of how this feature fits into the existing architecture",
518
+ "implementation_steps": ["Step 1: ...", "Step 2: ...", "Step 3: ..."],
519
+ "files_to_modify": [
520
+ {{
521
+ "path": "relative/path/to/file.py",
522
+ "action": "create|modify|extend",
523
+ "reason": "Why this file needs to be changed",
524
+ "details": "Specific changes needed (functions to add, classes to modify, etc.)"
525
+ }}
526
+ ],
527
+ "patterns_to_follow": ["Pattern 1 from codebase", "Pattern 2 from codebase"],
528
+ "integration_points": ["Where this integrates with existing code"]
529
+ }}
530
+
531
+ Be specific about file paths, actions, and implementation details."""
532
+
533
+ response = self.client.chat.completions.create(
534
+ model=self.model,
535
+ messages=[{"role": "user", "content": prompt}],
536
+ temperature=0.3
537
+ )
538
+
539
+ usage = response.usage
540
+ cost_tracker.add_architect_call(usage.prompt_tokens, usage.completion_tokens)
541
+
542
+ content = response.choices[0].message.content
543
+ try:
544
+ content = re.sub(r'^```json?\s*', '', content.strip())
545
+ content = re.sub(r'\s*```$', '', content)
546
+ return json.loads(content)
547
+ except:
548
+ return {"architecture_notes": content, "implementation_steps": [],
549
+ "files_to_modify": [], "patterns_to_follow": [], "integration_points": []}
550
+
551
+
552
+ class DeveloperLLM:
553
+ """LLM #2: Developer - code generation"""
554
+
555
+ def __init__(self, config: Config):
556
+ self.config = config
557
+ self._client = None
558
+ self.model = config.DEVELOPER_MODEL
559
+
560
+ @property
561
+ def client(self):
562
+ if self._client is None:
563
+ if not self.config.OPENAI_API_KEY:
564
+ raise ValueError("OpenAI API key not set!")
565
+ self._client = OpenAI(api_key=self.config.OPENAI_API_KEY)
566
+ return self._client
567
+
568
+ def reset_client(self):
569
+ self._client = None
570
+
571
+ def generate_boilerplate(self, ticket_analysis: Dict, strategy: Dict, code_context: List[Dict]) -> Dict[str, str]:
572
+ # Include more context from relevant files
573
+ context_str = "\n".join([
574
+ f"// File: {c['metadata'].get('file_path', '?')}\n{c['content'][:600]}\n"
575
+ for c in code_context[:5]
576
+ ])
577
+
578
+ files_to_modify = strategy.get('files_to_modify', [])
579
+ files_info = "\n".join([
580
+ f"- {f.get('path', 'unknown')}: {f.get('action', 'create')} - {f.get('reason', '')}"
581
+ for f in files_to_modify[:10]
582
+ ]) if files_to_modify else "Create new files as needed"
583
+
584
+ patterns = strategy.get('patterns_to_follow', [])
585
+ patterns_str = "\n".join([f"- {p}" for p in patterns]) if patterns else "Follow existing codebase patterns"
586
+
587
+ prompt = f"""Generate complete, production-ready implementation code for this ticket.
588
+
589
+ Ticket Summary: {ticket_analysis.get('summary', '')}
590
+ Key Entities: {', '.join(ticket_analysis.get('key_entities', []))}
591
+ Implementation Steps:
592
+ {chr(10).join(f"{i+1}. {step}" for i, step in enumerate(strategy.get('implementation_steps', [])))}
593
+
594
+ Files to Create/Modify:
595
+ {files_info}
596
+
597
+ Patterns to Follow:
598
+ {patterns_str}
599
+
600
+ Existing Codebase Patterns (for reference):
601
+ {context_str}
602
+
603
+ IMPORTANT REQUIREMENTS:
604
+ 1. Generate COMPLETE, WORKING code - NOT placeholder TODOs or comments
605
+ 2. Follow the exact patterns, structure, and style from the existing codebase
606
+ 3. Include all necessary imports, error handling, and type hints
607
+ 4. Make the code production-ready and functional
608
+ 5. For new files, include complete class/function implementations
609
+ 6. For modifications, show the complete updated code sections
610
+ 7. Use the same coding conventions, naming, and architecture as the existing code
611
+
612
+ Respond with JSON where keys are file paths and values are complete code:
613
+ {{"path/to/file.py": "complete working code here", "path/to/other.js": "complete working code here"}}
614
+
615
+ Generate actual implementation code, not TODO comments."""
616
+
617
+ response = self.client.chat.completions.create(
618
+ model=self.model,
619
+ messages=[{"role": "user", "content": prompt}],
620
+ temperature=0.3, # Slightly higher for more creative but still consistent code
621
+ max_tokens=4000 # Allow for longer, more complete code generation
622
+ )
623
+
624
+ usage = response.usage
625
+ cost_tracker.add_developer_call(usage.prompt_tokens, usage.completion_tokens)
626
+
627
+ content = response.choices[0].message.content
628
+ try:
629
+ # Clean up markdown code blocks
630
+ content = re.sub(r'^```json?\s*', '', content.strip())
631
+ content = re.sub(r'\s*```$', '', content)
632
+ code_dict = json.loads(content)
633
+
634
+ # Post-process: Ensure code quality and completeness
635
+ processed_code = {}
636
+ for file_path, code in code_dict.items():
637
+ # Check if code is mostly TODOs (more than 50% TODO lines)
638
+ lines = code.split('\n')
639
+ todo_count = sum(1 for line in lines if re.search(r'TODO:', line, re.IGNORECASE))
640
+ total_lines = len([l for l in lines if l.strip()])
641
+
642
+ if total_lines > 0 and (todo_count / total_lines) > 0.5:
643
+ # Code is mostly TODOs - add a note but keep it
644
+ processed_code[file_path] = f"# Note: This code contains many TODOs. Please review and implement.\n\n{code}"
645
+ else:
646
+ # Code looks good, return as-is
647
+ processed_code[file_path] = code
648
+
649
+ return processed_code
650
+ except json.JSONDecodeError:
651
+ # If JSON parsing fails, try to extract code blocks
652
+ code_blocks = re.findall(r'```(?:\w+)?\n(.*?)```', content, re.DOTALL)
653
+ if code_blocks:
654
+ return {"generated_code.txt": code_blocks[0]}
655
+ return {"generated_code.txt": content}
656
+ except Exception as e:
657
+ print(f"Warning: Error processing generated code: {e}")
658
+ return {"generated_code.txt": content}
659
+
660
+ def explain_code_context(self, code_context: List[Dict], question: str) -> str:
661
+ context_str = "\n".join([f"File: {c['metadata'].get('file_path', '?')}\n{c['content']}"
662
+ for c in code_context[:5]])
663
+
664
+ prompt = f"""Explain this code:
665
+
666
+ {context_str}
667
+
668
+ Question: {question}
669
+
670
+ Be concise and helpful."""
671
+
672
+ response = self.client.chat.completions.create(
673
+ model=self.model,
674
+ messages=[{"role": "user", "content": prompt}],
675
+ temperature=0.3
676
+ )
677
+
678
+ usage = response.usage
679
+ cost_tracker.add_developer_call(usage.prompt_tokens, usage.completion_tokens)
680
+
681
+ return response.choices[0].message.content
682
+
683
+
684
+ # ============================================================================
685
+ # Main Agent
686
+ # ============================================================================
687
+
688
+ class DevProductivityAgent:
689
+ """Main orchestrator with Pinecone and cost tracking"""
690
+
691
+ def __init__(self, config: Config = None):
692
+ self.config = config or Config()
693
+ self.indexer = CodebaseIndexer(self.config)
694
+ self.architect = ArchitectLLM(self.config)
695
+ self.developer = DeveloperLLM(self.config)
696
+
697
+ def set_api_keys(self, openai_key: str = None, pinecone_key: str = None):
698
+ """Set API keys"""
699
+ if openai_key:
700
+ self.config.OPENAI_API_KEY = openai_key
701
+ self.architect.reset_client()
702
+ self.developer.reset_client()
703
+ self.indexer._openai_client = None
704
+ if pinecone_key:
705
+ self.config.PINECONE_API_KEY = pinecone_key
706
+ self.indexer._index = None
707
+
708
+ def index_codebase(self, directory: str, extensions: List[str] = None) -> Dict:
709
+ print(f"πŸ“‚ Indexing: {directory}")
710
+ results = self.indexer.index_directory(directory, extensions)
711
+ stats = self.indexer.get_stats()
712
+ return {
713
+ "files_indexed": len([r for r in results.values() if isinstance(r, int)]),
714
+ "total_chunks": stats['total_chunks'],
715
+ "details": results
716
+ }
717
+
718
+ def process_ticket(self, ticket: JiraTicket) -> ImplementationPlan:
719
+ print("πŸ“‹ Analyzing...")
720
+ analysis = self.architect.analyze_ticket(ticket)
721
+
722
+ print("πŸ” Searching...")
723
+ queries = analysis.get('technical_keywords', []) + analysis.get('key_entities', [])
724
+
725
+ all_results = []
726
+ seen = set()
727
+ for q in queries[:5]:
728
+ for r in self.indexer.search(q, top_k=5):
729
+ fp = r['metadata'].get('file_path', '')
730
+ if fp not in seen:
731
+ all_results.append(r)
732
+ seen.add(fp)
733
+
734
+ print("πŸ“ Planning...")
735
+ strategy = self.architect.create_implementation_strategy(analysis, all_results)
736
+
737
+ print("πŸ’» Generating...")
738
+ code = self.developer.generate_boilerplate(analysis, strategy, all_results)
739
+
740
+ cost_tracker.record_ticket()
741
+
742
+ return ImplementationPlan(
743
+ ticket_summary=analysis.get('summary', ''),
744
+ key_entities=analysis.get('key_entities', []),
745
+ relevant_files=[{
746
+ 'path': r['metadata'].get('file_path', ''),
747
+ 'relevance': f"Lines {r['metadata'].get('line_start', '?')}-{r['metadata'].get('line_end', '?')}",
748
+ 'preview': r['content'][:200]
749
+ } for r in all_results[:10]],
750
+ implementation_steps=strategy.get('implementation_steps', []),
751
+ prerequisites=analysis.get('prerequisites', []),
752
+ boilerplate_code=code,
753
+ architecture_notes=strategy.get('architecture_notes', ''),
754
+ estimated_complexity=analysis.get('complexity', 'Unknown')
755
+ )
756
+
757
+ def ask_about_code(self, question: str) -> str:
758
+ results = self.indexer.search(question)
759
+ if not results:
760
+ return "No relevant code found. Index your codebase first."
761
+ answer = self.developer.explain_code_context(results, question)
762
+ cost_tracker.record_question()
763
+ return answer
764
+
765
+ def get_cost_stats(self) -> Dict:
766
+ return cost_tracker.get_stats()
767
+
768
+ def reset_cost_tracking(self):
769
+ cost_tracker.reset()
770
+
771
+
772
+ # ============================================================================
773
+ # FastAPI
774
+ # ============================================================================
775
+
776
+ app = FastAPI(title="Developer Productivity Agent", version="2.0.0")
777
+ app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True,
778
+ allow_methods=["*"], allow_headers=["*"])
779
+
780
+ agent = DevProductivityAgent()
781
+
782
+ @app.get("/")
783
+ async def root():
784
+ stats = agent.indexer.get_stats()
785
+ return {"status": "healthy", "vector_db": "Pinecone", "chunks": stats['total_chunks']}
786
+
787
+ @app.get("/stats")
788
+ async def get_stats():
789
+ return agent.indexer.get_stats()
790
+
791
+ @app.get("/cost-analytics")
792
+ async def get_cost_analytics():
793
+ """Get cost analytics and savings"""
794
+ return agent.get_cost_stats()
795
+
796
+ @app.post("/reset-costs")
797
+ async def reset_costs():
798
+ agent.reset_cost_tracking()
799
+ return {"status": "reset"}
800
+
801
+ @app.post("/index")
802
+ async def index_codebase(directory: str, extensions: List[str] = None):
803
+ try:
804
+ return {"status": "success", "results": agent.index_codebase(directory, extensions)}
805
+ except Exception as e:
806
+ raise HTTPException(status_code=500, detail=str(e))
807
+
808
+ @app.post("/process-ticket", response_model=ImplementationPlan)
809
+ async def process_ticket(ticket: JiraTicket):
810
+ try:
811
+ return agent.process_ticket(ticket)
812
+ except Exception as e:
813
+ raise HTTPException(status_code=500, detail=str(e))
814
+
815
+ @app.post("/ask")
816
+ async def ask(question: str):
817
+ try:
818
+ return {"answer": agent.ask_about_code(question)}
819
+ except Exception as e:
820
+ raise HTTPException(status_code=500, detail=str(e))
821
+
822
+ @app.post("/search")
823
+ async def search(query: str, top_k: int = 10):
824
+ try:
825
+ return {"results": agent.indexer.search(query, top_k)}
826
+ except Exception as e:
827
+ raise HTTPException(status_code=500, detail=str(e))
828
+
829
+ @app.delete("/clear")
830
+ async def clear():
831
+ agent.indexer.clear_index()
832
+ return {"status": "cleared"}
833
+
834
+ if __name__ == "__main__":
835
+ import argparse
836
+ parser = argparse.ArgumentParser()
837
+ parser.add_argument("--index", type=str)
838
+ parser.add_argument("--serve", action="store_true")
839
+ parser.add_argument("--port", type=int, default=8000)
840
+ args = parser.parse_args()
841
+
842
+ if args.index:
843
+ agent.index_codebase(args.index)
844
+ if args.serve:
845
+ uvicorn.run(app, host="0.0.0.0", port=args.port)