Pipalskill commited on
Commit
990a697
·
verified ·
1 Parent(s): 02d3c99

Create agent_langchain.py

Browse files
Files changed (1) hide show
  1. agent_langchain.py +420 -0
agent_langchain.py ADDED
@@ -0,0 +1,420 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ os.environ["TOKENIZERS_PARALLELISM"] = "false"
3
+ os.environ["TRANSFORMERS_CACHE"] = "/tmp/transformers"
4
+ os.environ["HF_HOME"] = "/tmp/huggingface"
5
+ os.environ["SENTENCE_TRANSFORMERS_HOME"] = "/tmp/sentence_transformers"
6
+ os.environ["TORCH_HOME"] = "/tmp/torch"
7
+
8
+ import requests
9
+ import torch
10
+ import time
11
+ from transformers import AutoTokenizer, AutoModelForSequenceClassification
12
+ import numpy as np
13
+ from sentence_transformers import SentenceTransformer
14
+ import chromadb
15
+ from chromadb.config import Settings
16
+ from langchain_google_genai import ChatGoogleGenerativeAI
17
+ from langchain.agents import AgentExecutor, create_react_agent
18
+ from langchain.tools import Tool
19
+ from langchain.prompts import PromptTemplate
20
+ import threading
21
+ from datetime import datetime
22
+ import firebase_admin
23
+ from firebase_admin import credentials, firestore
24
+ from typing import Optional, Dict, Any
25
+
26
+ # Environment
27
+ GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
28
+ ROUTING_URL = os.environ.get("ROUTING_URL")
29
+ SPACE_URL = os.environ.get("SPACE_URL", "http://localhost:7860")
30
+ FIREBASE_CREDS_PATH = os.environ.get("FIREBASE_CREDS_PATH")
31
+
32
+ # Initialize Firebase
33
+ db = None
34
+ if FIREBASE_CREDS_PATH and os.path.exists(FIREBASE_CREDS_PATH):
35
+ try:
36
+ if not firebase_admin._apps:
37
+ cred = credentials.Certificate(FIREBASE_CREDS_PATH)
38
+ firebase_admin.initialize_app(cred)
39
+ db = firestore.client()
40
+ print("✅ Firebase initialized")
41
+ except Exception as e:
42
+ print(f"⚠️ Firebase init failed: {e}")
43
+
44
+ # Label Dictionary
45
+ LABEL_DICTIONARY = {
46
+ "I1": "Low Impact", "I2": "Medium Impact", "I3": "High Impact", "I4": "Critical Impact",
47
+ "U1": "Low Urgency", "U2": "Medium Urgency", "U3": "High Urgency", "U4": "Critical Urgency",
48
+ "T1": "Information", "T2": "Incident", "T3": "Problem", "T4": "Request", "T5": "Question"
49
+ }
50
+
51
+ # Classification Model
52
+ clf_model_name = "DavinciTech/BERT_Categorizer"
53
+ clf_tokenizer = AutoTokenizer.from_pretrained(clf_model_name, cache_dir="/tmp/transformers")
54
+ clf_model = AutoModelForSequenceClassification.from_pretrained(clf_model_name, cache_dir="/tmp/transformers")
55
+
56
+ def classify_ticket(text):
57
+ """Classify ticket into Impact, Urgency, and Type."""
58
+ inputs = clf_tokenizer(text, return_tensors="pt", truncation=True)
59
+ outputs = clf_model(**inputs)
60
+ logits = outputs.logits[0]
61
+ impact_idx = torch.argmax(logits[:4]).item() + 1
62
+ urgency_idx = torch.argmax(logits[4:8]).item() + 1
63
+ type_idx = torch.argmax(logits[8:]).item() + 1
64
+ return {
65
+ "impact": LABEL_DICTIONARY[f"I{impact_idx}"],
66
+ "urgency": LABEL_DICTIONARY[f"U{urgency_idx}"],
67
+ "type": LABEL_DICTIONARY[f"T{type_idx}"]
68
+ }
69
+
70
+ # Routing Function
71
+ def call_routing(text, retries=3, delay=5):
72
+ """Route ticket to appropriate department."""
73
+ url = ROUTING_URL if ROUTING_URL else f"{SPACE_URL}/route"
74
+ for attempt in range(retries):
75
+ try:
76
+ resp = requests.post(url, json={"text": text}, timeout=30)
77
+ resp.raise_for_status()
78
+ return resp.json().get("department", "General IT")
79
+ except Exception as e:
80
+ print(f"Routing attempt {attempt+1} failed: {e}")
81
+ if attempt < retries - 1:
82
+ time.sleep(delay)
83
+ return "General IT"
84
+
85
+ # Knowledge Base
86
+ CHROMA_PATH = "/tmp/chroma"
87
+ COLLECTION_NAME = "knowledge_base"
88
+ kb_collection = None
89
+ kb_lock = threading.Lock()
90
+ encoder = SentenceTransformer("all-MiniLM-L6-v2", cache_folder="/tmp/sentence_transformers")
91
+
92
+ def get_kb_collection():
93
+ global kb_collection
94
+ if kb_collection is None:
95
+ with kb_lock:
96
+ if kb_collection is None:
97
+ try:
98
+ chroma_client = chromadb.PersistentClient(
99
+ path=CHROMA_PATH,
100
+ settings=Settings(anonymized_telemetry=False, allow_reset=True)
101
+ )
102
+ kb_collection = chroma_client.get_or_create_collection(COLLECTION_NAME)
103
+ except Exception as e:
104
+ print(f"Could not get KB collection: {e}")
105
+ return kb_collection
106
+
107
+ def query_kb(text: str, top_k: int = 1):
108
+ """Query KB and return answer with confidence."""
109
+ collection = get_kb_collection()
110
+ if not collection or collection.count() == 0:
111
+ return {"answer": None, "confidence": 0.0}
112
+
113
+ try:
114
+ query_embedding = encoder.encode([text])[0].tolist()
115
+ results = collection.query(
116
+ query_embeddings=[query_embedding],
117
+ n_results=top_k,
118
+ include=["documents", "distances", "metadatas"]
119
+ )
120
+
121
+ if not results or not results.get("documents") or len(results["documents"][0]) == 0:
122
+ return {"answer": None, "confidence": 0.0}
123
+
124
+ answer = results["documents"][0][0]
125
+ distance = results["distances"][0][0] if results.get("distances") else 1.0
126
+ confidence = max(0.0, 1.0 - (distance / 2.0))
127
+
128
+ return {"answer": answer, "confidence": round(float(confidence), 3)}
129
+ except Exception as e:
130
+ print(f"KB query failed: {e}")
131
+ return {"answer": None, "confidence": 0.0}
132
+
133
+ # Firestore Helper
134
+ def save_ticket_to_firestore(ticket_data: Dict[str, Any]):
135
+ """Save resolved/escalated ticket to Firestore."""
136
+ if not db:
137
+ print("⚠️ Firestore not initialized, skipping save")
138
+ return None
139
+
140
+ try:
141
+ ticket_ref = db.collection('tickets').document()
142
+ ticket_data['created_at'] = firestore.SERVER_TIMESTAMP
143
+ ticket_data['updated_at'] = firestore.SERVER_TIMESTAMP
144
+ ticket_ref.set(ticket_data)
145
+ print(f"✅ Ticket saved to Firestore: {ticket_ref.id}")
146
+ return ticket_ref.id
147
+ except Exception as e:
148
+ print(f"❌ Firestore save failed: {e}")
149
+ return None
150
+
151
+ # Gemini LLM
152
+ llm = ChatGoogleGenerativeAI(
153
+ model="gemini-2.0-flash-exp",
154
+ temperature=0.3,
155
+ google_api_key=GEMINI_API_KEY
156
+ )
157
+
158
+ # Global conversation storage
159
+ conversations = {}
160
+
161
+ # Tool Functions for Agent (TRULY AUTONOMOUS NOW)
162
+ def classify_tool(query: str) -> str:
163
+ """Analyzes ticket severity, impact, urgency, and type. Use when you need to understand ticket priority."""
164
+ result = classify_ticket(query)
165
+ return f"Impact: {result['impact']}, Urgency: {result['urgency']}, Type: {result['type']}"
166
+
167
+ def routing_tool(query: str) -> str:
168
+ """Identifies which IT department should handle this issue. Use when you need to know responsible team."""
169
+ dept = call_routing(query)
170
+ return f"Department: {dept}"
171
+
172
+ def kb_tool(query: str) -> str:
173
+ """Searches knowledge base for solutions. Returns answer with confidence score. Use when you need technical solutions."""
174
+ result = query_kb(query)
175
+ if result["answer"] and result["confidence"] > 0.5:
176
+ return f"[KB Confidence: {result['confidence']}]\n{result['answer']}"
177
+ return f"[KB Confidence: {result['confidence']}] No relevant solution found in knowledge base."
178
+
179
+ def escalation_tool(reason: str) -> str:
180
+ """Creates escalation ticket for human agent. Use when: KB confidence is low, issue is complex, or user reports solution failed."""
181
+ ticket_id = f"TKT-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
182
+ return f"ESCALATED: Ticket {ticket_id} created. Reason: {reason}. Human agent will respond in 2-4 hours."
183
+
184
+ # Define Tools with better descriptions
185
+ tools = [
186
+ Tool(
187
+ name="ClassifyTicket",
188
+ func=classify_tool,
189
+ description="Analyzes ticket to determine impact level, urgency, and type. Use this when you need to understand the severity or priority of an issue."
190
+ ),
191
+ Tool(
192
+ name="RouteTicket",
193
+ func=routing_tool,
194
+ description="Determines which IT department should handle this ticket. Use this when you need to identify the responsible team."
195
+ ),
196
+ Tool(
197
+ name="SearchKnowledgeBase",
198
+ func=kb_tool,
199
+ description="Searches internal knowledge base for solutions. Returns answer with confidence score (0-1). Use this when you need to find technical solutions or troubleshooting steps."
200
+ ),
201
+ Tool(
202
+ name="EscalateToHuman",
203
+ func=escalation_tool,
204
+ description="Creates an escalation ticket for human agent review. Use this ONLY when: 1) KB confidence score is below 0.75, 2) Issue is highly complex or unusual, 3) User confirms solution didn't work."
205
+ )
206
+ ]
207
+
208
+ # IMPROVED Agent Prompt - More Autonomous
209
+ AGENT_PROMPT = """You are an intelligent IT Helpdesk AI Agent. Your goal is to efficiently resolve IT support tickets using the tools available to you.
210
+
211
+ AVAILABLE TOOLS:
212
+ {tools}
213
+
214
+ TOOL NAMES: {tool_names}
215
+
216
+ GUIDING PRINCIPLES:
217
+ 1. **Think autonomously** - Decide which tools you need based on the situation, not a fixed sequence
218
+ 2. **Be efficient** - Only use tools when they add value to solving the user's problem
219
+ 3. **Trust high-confidence solutions** - If KB returns confidence >= 0.75, provide that solution
220
+ 4. **Escalate wisely** - Only escalate when truly necessary (low KB confidence, complex issues, or failed solutions)
221
+ 5. **Maintain context** - Remember previous conversation history when handling follow-ups
222
+ 6. **Be empathetic** - Users are frustrated when things break; be professional and supportive
223
+
224
+ DECISION FRAMEWORK:
225
+ - For a NEW ticket: You might need to classify, route, search KB - but decide based on what's needed
226
+ - For FOLLOW-UPS: If user says solution worked → close positively. If it failed → search KB again or escalate
227
+ - For SIMPLE questions: You may not need all tools - use your judgment
228
+ - For COMPLEX issues: Use multiple tools to gather information before providing solution
229
+
230
+ FORMAT:
231
+ Question: the user's input
232
+ Thought: your reasoning about what to do next
233
+ Action: the tool to use (must be one of [{tool_names}])
234
+ Action Input: the input for that tool
235
+ Observation: the tool's output
236
+ ... (repeat Thought/Action/Observation as needed)
237
+ Thought: I now have enough information to respond
238
+ Final Answer: your complete response to the user
239
+
240
+ IMPORTANT:
241
+ - Don't mention tool names or technical process to users
242
+ - Provide clear, step-by-step instructions
243
+ - Ask clarifying questions if needed
244
+ - Be conversational and helpful
245
+
246
+ Begin!
247
+
248
+ Question: {input}
249
+ Thought: {agent_scratchpad}"""
250
+
251
+ prompt = PromptTemplate.from_template(AGENT_PROMPT)
252
+
253
+ # Create Agent with more flexibility
254
+ agent = create_react_agent(llm=llm, tools=tools, prompt=prompt)
255
+ agent_executor = AgentExecutor(
256
+ agent=agent,
257
+ tools=tools,
258
+ verbose=True,
259
+ max_iterations=8, # Increased for more autonomy
260
+ handle_parsing_errors=True,
261
+ return_intermediate_steps=True,
262
+ early_stopping_method="generate" # Allow agent to decide when to stop
263
+ )
264
+
265
+ # Main Processing Function
266
+ def process_with_agent(
267
+ user_message: str,
268
+ conversation_id: str = None,
269
+ user_email: str = None,
270
+ callback=None # For streaming updates via WebSocket
271
+ ):
272
+ """Process user message through autonomous AI agent."""
273
+
274
+ if not conversation_id:
275
+ conversation_id = f"conv_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{hash(user_message) % 10000}"
276
+
277
+ if conversation_id not in conversations:
278
+ conversations[conversation_id] = {
279
+ "messages": [],
280
+ "ticket_info": {},
281
+ "created_at": datetime.now().isoformat(),
282
+ "user_email": user_email,
283
+ "status": "open"
284
+ }
285
+
286
+ conv = conversations[conversation_id]
287
+
288
+ conv["messages"].append({
289
+ "role": "user",
290
+ "content": user_message,
291
+ "timestamp": datetime.now().isoformat()
292
+ })
293
+
294
+ if callback:
295
+ callback({"type": "status", "message": "Agent is thinking..."})
296
+
297
+ # Build context for follow-ups
298
+ if len(conv["messages"]) > 1:
299
+ context = f"CONVERSATION HISTORY:\n"
300
+ for msg in conv["messages"][-6:-1]: # Last 5 messages for context
301
+ context += f"{msg['role'].upper()}: {msg['content']}\n"
302
+ context += f"\nCURRENT MESSAGE: {user_message}"
303
+ agent_input = context
304
+ else:
305
+ agent_input = user_message
306
+
307
+ try:
308
+ result = agent_executor.invoke({"input": agent_input})
309
+
310
+ agent_response = result.get("output", "I apologize, I encountered an error.")
311
+ intermediate_steps = result.get("intermediate_steps", [])
312
+
313
+ # Determine status and handle Firestore
314
+ status = "in_progress"
315
+ should_save = False
316
+
317
+ # Check for resolution indicators
318
+ if any(phrase in agent_response.lower() for phrase in ["resolved", "you're all set", "should work now", "problem solved"]):
319
+ status = "resolved"
320
+ should_save = True
321
+ elif "ESCALATED" in agent_response or "TKT-" in agent_response:
322
+ status = "escalated"
323
+ should_save = True
324
+
325
+ # Extract ticket info from tools
326
+ ticket_info = conv.get("ticket_info", {})
327
+ for action, observation in intermediate_steps:
328
+ if action.tool == "ClassifyTicket":
329
+ # Parse classification
330
+ parts = str(observation).split(", ")
331
+ for part in parts:
332
+ if "Impact:" in part:
333
+ ticket_info["impact"] = part.split(": ")[1]
334
+ elif "Urgency:" in part:
335
+ ticket_info["urgency"] = part.split(": ")[1]
336
+ elif "Type:" in part:
337
+ ticket_info["type"] = part.split(": ")[1]
338
+ elif action.tool == "RouteTicket":
339
+ ticket_info["department"] = str(observation).replace("Department: ", "")
340
+
341
+ conv["ticket_info"] = ticket_info
342
+ conv["status"] = status
343
+
344
+ reasoning_trace = []
345
+ for action, observation in intermediate_steps:
346
+ reasoning_trace.append({
347
+ "tool": action.tool,
348
+ "input": action.tool_input,
349
+ "output": str(observation)[:200]
350
+ })
351
+
352
+ if callback:
353
+ callback({
354
+ "type": "tool_use",
355
+ "tool": action.tool,
356
+ "input": action.tool_input
357
+ })
358
+
359
+ conv["messages"].append({
360
+ "role": "assistant",
361
+ "content": agent_response,
362
+ "timestamp": datetime.now().isoformat(),
363
+ "reasoning": reasoning_trace
364
+ })
365
+
366
+ # Save to Firestore if resolved/escalated
367
+ firestore_id = None
368
+ if should_save:
369
+ firestore_data = {
370
+ "conversation_id": conversation_id,
371
+ "status": status,
372
+ "user_email": user_email or "anonymous",
373
+ "ticket_info": ticket_info,
374
+ "messages": conv["messages"],
375
+ "resolution": agent_response,
376
+ "created_at_iso": conv["created_at"]
377
+ }
378
+ firestore_id = save_ticket_to_firestore(firestore_data)
379
+
380
+ if callback:
381
+ callback({
382
+ "type": "saved",
383
+ "firestore_id": firestore_id
384
+ })
385
+
386
+ return {
387
+ "conversation_id": conversation_id,
388
+ "response": agent_response,
389
+ "status": status,
390
+ "message_count": len(conv["messages"]),
391
+ "reasoning_trace": reasoning_trace,
392
+ "ticket_info": ticket_info,
393
+ "firestore_id": firestore_id
394
+ }
395
+
396
+ except Exception as e:
397
+ print(f"Agent error: {e}")
398
+ import traceback
399
+ traceback.print_exc()
400
+
401
+ error_response = "I apologize, I encountered an error. Please try again or I can escalate this to a human agent."
402
+ conv["messages"].append({
403
+ "role": "assistant",
404
+ "content": error_response,
405
+ "timestamp": datetime.now().isoformat()
406
+ })
407
+
408
+ if callback:
409
+ callback({"type": "error", "message": str(e)})
410
+
411
+ return {
412
+ "conversation_id": conversation_id,
413
+ "response": error_response,
414
+ "status": "error",
415
+ "error": str(e)
416
+ }
417
+
418
+ def get_conversation_history(conversation_id: str):
419
+ """Get conversation history."""
420
+ return conversations.get(conversation_id)