File size: 7,475 Bytes
24f4ff5
9850c97
 
 
 
 
 
24f4ff5
573205a
aa4ac8d
1e4ae98
aa4ac8d
 
bd9c6ae
24f4ff5
cb63650
9850c97
1e4ae98
 
 
9850c97
 
1e4ae98
 
9850c97
aa4ac8d
cb63650
aa4ac8d
 
 
 
1e4ae98
 
aa4ac8d
1e4ae98
a439804
8c67f6c
 
aa4ac8d
 
 
 
2b51b85
1e4ae98
005b7c5
2b51b85
 
3e0cda0
 
eac3167
3e0cda0
eac3167
3e0cda0
9850c97
 
1e4ae98
9850c97
eac3167
3e0cda0
eac3167
 
 
4376f5c
3e0cda0
 
1e4ae98
eac3167
0680215
9850c97
 
 
 
0680215
1e4ae98
0680215
eac3167
1e4ae98
9850c97
 
 
3e0cda0
eac3167
3e0cda0
 
eac3167
1e4ae98
9850c97
1e4ae98
9850c97
 
3e0cda0
1e4ae98
 
3e0cda0
eac3167
 
005b7c5
9850c97
 
 
aa4ac8d
 
cb63650
1e4ae98
 
 
 
cb63650
 
 
 
 
1e4ae98
 
 
 
 
 
 
 
 
 
 
 
cb63650
1e4ae98
cb63650
 
1e4ae98
 
 
 
 
 
 
 
 
 
aa4ac8d
1e4ae98
 
 
 
 
 
 
 
 
 
 
 
 
aa4ac8d
1e4ae98
 
 
aa4ac8d
 
1e4ae98
aa4ac8d
 
 
 
 
1e4ae98
aa4ac8d
 
 
 
 
1e4ae98
9850c97
1e4ae98
aa4ac8d
eac3167
 
9850c97
 
 
eac3167
 
 
 
9850c97
eac3167
 
 
9850c97
 
eac3167
1e4ae98
eac3167
 
9850c97
 
 
aa4ac8d
 
 
 
 
 
9850c97
 
 
 
 
 
 
1e4ae98
cb63650
 
9850c97
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
import os
os.environ["TOKENIZERS_PARALLELISM"] = "false"
os.environ["TRANSFORMERS_CACHE"] = "/tmp/transformers"
os.environ["HF_HOME"] = "/tmp/huggingface"
os.environ["SENTENCE_TRANSFORMERS_HOME"] = "/tmp/sentence_transformers"
os.environ["TORCH_HOME"] = "/tmp/torch"

import json
from fastapi import FastAPI, HTTPException, UploadFile, File
from pydantic import BaseModel
from typing import Optional
import chromadb
from chromadb.config import Settings
from sentence_transformers import SentenceTransformer

# Import from autonomous agent
from agent_langchain import (
    process_with_agent,
    get_conversation_history,
    classify_ticket,
    call_routing,
    get_kb_collection,
    encoder,
    conversations
)

app = FastAPI(title="Smart Helpdesk AI Agent - Autonomous")

# Request Models
class TicketRequest(BaseModel):
    text: str
    conversation_id: Optional[str] = None
    user_email: Optional[str] = None

# Persistent Chroma settings
CHROMA_PATH = "/tmp/chroma"
COLLECTION_NAME = "knowledge_base"

# -------------------------------
# KB Setup Endpoint
# -------------------------------
@app.post("/setup")
async def setup_kb(kb_file: UploadFile = File(...)):
    """Upload and index knowledge base."""
    try:
        content_bytes = await kb_file.read()
        data = json.loads(content_bytes)

        if not isinstance(data, list):
            raise HTTPException(status_code=400, detail="JSON must be a list of items.")

        print(f"📘 Loaded {len(data)} items from {kb_file.filename}")

        chroma_client = chromadb.PersistentClient(
            path=CHROMA_PATH,
            settings=Settings(anonymized_telemetry=False, allow_reset=True)
        )
        collection = chroma_client.get_or_create_collection(COLLECTION_NAME)

        if collection.count() > 0:
            print(f"🧹 Clearing {collection.count()} existing records...")
            collection.delete(ids=collection.get()['ids'])

        texts, ids, metadatas = [], [], []
        for i, item in enumerate(data):
            text = item.get("answer") or item.get("text") or item.get("content") or ""
            item_id = item.get("id") or str(i)
            category = item.get("category", "")
            
            if not text:
                print(f"⚠️ Skipping item {i} - no text content")
                continue
            
            combined_text = f"Category: {category}. {text}" if category else text
            texts.append(combined_text)
            ids.append(str(item_id))
            metadatas.append({"id": str(item_id), "category": category, "original_index": i})

        if not texts:
            raise HTTPException(status_code=400, detail="No valid text content found in JSON.")

        print("🧠 Generating embeddings...")
        embeddings = encoder.encode(texts, show_progress_bar=True).tolist()

        print("💾 Adding to ChromaDB...")
        collection.add(ids=ids, embeddings=embeddings, documents=texts, metadatas=metadatas)

        # Update global reference
        import agent_langchain
        agent_langchain.kb_collection = collection

        print(f"✅ Successfully added {collection.count()} records")
        return {"message": "Knowledge base initialized", "count": collection.count()}

    except json.JSONDecodeError:
        raise HTTPException(status_code=400, detail="Invalid JSON file.")
    except Exception as e:
        import traceback
        traceback.print_exc()
        raise HTTPException(status_code=500, detail=f"Setup failed: {str(e)}")

# -------------------------------
# MAIN ORCHESTRATE ENDPOINT (Autonomous Agent)
# -------------------------------
@app.post("/orchestrate")
async def orchestrate_endpoint(ticket: TicketRequest):
    """
    Main AI Agent endpoint - fully autonomous:
    - Decides its own workflow
    - Handles multi-turn conversations
    - Auto-escalates when needed
    - Maintains context
    """
    try:
        result = process_with_agent(
            user_message=ticket.text,
            conversation_id=ticket.conversation_id
        )
        
        return {
            "conversation_id": result["conversation_id"],
            "response": result["response"],
            "status": result["status"],
            "message_count": result["message_count"],
            "reasoning_trace": result.get("reasoning_trace", []),
            "instructions": {
                "continue_conversation": "Include the conversation_id in your next request",
                "new_ticket": "Omit conversation_id to start fresh"
            }
        }
        
    except Exception as e:
        import traceback
        traceback.print_exc()
        raise HTTPException(status_code=500, detail=f"Agent failed: {str(e)}")

# -------------------------------
# Get Conversation History
# -------------------------------
@app.get("/conversation/{conversation_id}")
async def get_conversation(conversation_id: str):
    """Retrieve full conversation history."""
    conv = get_conversation_history(conversation_id)
    if not conv:
        raise HTTPException(status_code=404, detail="Conversation not found")
    
    return {
        "conversation_id": conversation_id,
        "messages": conv["messages"],
        "created_at": conv["created_at"],
        "message_count": len(conv["messages"])
    }

# -------------------------------
# Individual Tool Endpoints (for testing)
# -------------------------------
@app.post("/classify")
async def classify_endpoint(ticket: TicketRequest):
    """Test classification only."""
    classification = classify_ticket(ticket.text)
    return {"classification": classification}

@app.post("/route")
async def route_endpoint(ticket: TicketRequest):
    """Test routing only."""
    department = call_routing(ticket.text)
    return {"department": department}

@app.post("/kb_query")
async def kb_query_endpoint(ticket: TicketRequest):
    """Test KB query only."""
    collection = get_kb_collection()
    if not collection or collection.count() == 0:
        raise HTTPException(status_code=400, detail="KB not set up. Call /setup first.")

    try:
        query_embedding = encoder.encode([ticket.text])[0].tolist()
        result = collection.query(
            query_embeddings=[query_embedding],
            n_results=1,
            include=["documents", "distances", "metadatas"]
        )

        if not result or not result.get('documents') or len(result['documents'][0]) == 0:
            return {"answer": "No relevant KB found.", "confidence": 0.0}

        best_doc = result['documents'][0][0]
        best_distance = result['distances'][0][0] if result.get('distances') else 1.0
        confidence = max(0.0, 1.0 - (best_distance / 2.0))

        return {"answer": best_doc, "confidence": round(float(confidence), 3)}

    except Exception as e:
        import traceback
        traceback.print_exc()
        raise HTTPException(status_code=500, detail=f"KB query failed: {str(e)}")

# -------------------------------
# Health Check
# -------------------------------
@app.get("/health")
async def health():
    collection = get_kb_collection()
    kb_status = "initialized" if collection and collection.count() > 0 else "not initialized"
    kb_count = collection.count() if collection else 0
    
    return {
        "status": "ok",
        "kb_status": kb_status,
        "kb_records": kb_count,
        "active_conversations": len(conversations),
        "agent_type": "Autonomous ReAct Agent with Gemini"
    }