Skip to content

Commit 2f69f7f

Browse files
author
waleedahmedhere
committed
feat: hybrid strategic + semantic search mode
1 parent b966cb8 commit 2f69f7f

2 files changed

Lines changed: 180 additions & 120 deletions

File tree

api/main.py

Lines changed: 100 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import os
22
import time
3-
from typing import List, Optional
3+
from pathlib import Path
4+
from typing import List
45
from fastapi import FastAPI, HTTPException
56
from fastapi.middleware.cors import CORSMiddleware
67
from fastapi.staticfiles import StaticFiles
@@ -59,6 +60,15 @@ async def serve_css():
5960

6061
qdrant_client = QdrantClient(url=QDRANT_URL, api_key=QDRANT_API_KEY, timeout=30)
6162

63+
# Load master summary once at startup
64+
MASTER_SUMMARY = ""
65+
master_summary_path = Path("data/clean/master_summary.txt")
66+
if master_summary_path.exists():
67+
MASTER_SUMMARY = master_summary_path.read_text(encoding="utf-8")
68+
print(f"✅ Master summary loaded: {len(MASTER_SUMMARY)} characters")
69+
else:
70+
print("⚠️ Master summary not found. Run generate_master_summary.py first.")
71+
6272
# Session memory storage
6373
session_memories: dict[str, list] = {}
6474

@@ -67,38 +77,42 @@ def get_history(session_id: str) -> list:
6777
session_memories[session_id] = []
6878
return session_memories[session_id]
6979

70-
# ---------- Prompt Template ----------
71-
SALES_PROMPT_TEMPLATE = (
72-
"You are a Sales Assistant AI that helps sales representatives query information from previous client meetings. "
73-
"You have access to meeting transcripts, summaries, and client interactions to provide insights about:\n\n"
74-
75-
"• Client objections, concerns, and feedback\n"
76-
"• Pricing discussions and negotiations\n"
77-
"• Feature requests and product feedback\n"
78-
"• Competitor comparisons mentioned by clients\n"
79-
"• Implementation concerns and technical questions\n"
80-
"• Industry-specific requirements and use cases\n"
81-
"• Decision-making processes and timelines\n"
82-
"• Stakeholder involvement and buying committees\n\n"
83-
84-
"RESPONSE GUIDELINES:\n"
85-
"• Provide specific, actionable insights from the meeting data\n"
86-
"• When possible, reference specific client meetings or contexts\n"
87-
"• Highlight patterns across multiple client interactions\n"
88-
"• Be concise but comprehensive in your responses\n"
89-
"• If you don't have relevant information, clearly state that\n"
90-
"• Focus on helping sales reps prepare for future meetings\n"
91-
"• Suggest follow-up questions or strategies when appropriate\n\n"
92-
93-
"EXAMPLE RESPONSES:\n"
94-
"• 'Based on 3 recent enterprise meetings, clients commonly ask about data security certifications...'\n"
95-
"• 'In the TechCorp meeting last month, they mentioned budget constraints around Q4...'\n"
96-
"• 'Several healthcare clients have requested HIPAA compliance documentation...'\n\n"
97-
98-
"Context from previous meetings:\n{context}\n\n"
80+
# ---------- Query classifier ----------
81+
STRATEGIC_KEYWORDS = [
82+
"strategy", "strategies", "recommend", "suggestion", "suggest", "improve", "improvement",
83+
"pattern", "trend", "common", "most", "top", "best", "worst", "typical", "usually",
84+
"objection", "objections", "pricing", "price", "feature", "features", "competitor",
85+
"competitors", "region", "industry", "industries", "segment", "marketing", "pitch",
86+
"plan", "approach", "how should", "what should", "why do", "which clients",
87+
"overall", "across", "all clients", "all meetings", "generally", "insight", "insights",
88+
"win", "lose", "lost", "deal", "convert", "conversion", "sales cycle", "follow up"
89+
]
90+
91+
def is_strategic_question(message: str) -> bool:
92+
msg = message.lower()
93+
return any(kw in msg for kw in STRATEGIC_KEYWORDS)
94+
95+
# ---------- Prompt Templates ----------
96+
STRATEGIC_PROMPT = (
97+
"You are an expert Sales Strategist AI for Enatega — a food delivery platform solution.\n"
98+
"You have comprehensive knowledge from 250+ real client sales meetings summarized below.\n\n"
99+
"Use this knowledge to give specific, actionable, data-driven answers.\n"
100+
"Reference patterns, client names, regions, and real examples from the data.\n"
101+
"If asked for a plan or strategy, provide structured step-by-step guidance.\n\n"
102+
"KNOWLEDGE BASE FROM 250+ MEETINGS:\n{master_summary}\n\n"
103+
"Chat History:\n{chat_history}\n\n"
104+
"Question: {question}\n"
105+
"Answer:"
106+
)
107+
108+
SPECIFIC_PROMPT = (
109+
"You are a Sales Assistant AI for Enatega — a food delivery platform solution.\n"
110+
"Answer the question using the relevant meeting excerpts provided below.\n"
111+
"Be specific, reference client names and meetings where relevant.\n\n"
112+
"Relevant Meeting Excerpts:\n{context}\n\n"
99113
"Chat History:\n{chat_history}\n\n"
100-
"Sales Rep Question: {question}\n"
101-
"Assistant:"
114+
"Question: {question}\n"
115+
"Answer:"
102116
)
103117

104118
# ---------- request/response ----------
@@ -115,106 +129,72 @@ class ChatResp(BaseModel):
115129
@app.post("/chat", response_model=ChatResp)
116130
async def chat_endpoint(req: ChatReq):
117131
start_time = time.time()
118-
132+
119133
try:
120-
# Simple similarity search using qdrant client directly
121-
print(f"Searching for: {req.message}")
122-
123-
try:
124-
query_vector = embeddings.embed_query(req.message)
125-
print("Query vector created successfully")
126-
except Exception as embed_error:
127-
print(f"Embedding error: {embed_error}")
128-
raise HTTPException(status_code=500, detail=f"Embedding failed: {str(embed_error)}")
129-
130-
try:
131-
search_result = qdrant_client.query_points(
132-
collection_name=COLLECTION_NAME,
133-
query=query_vector,
134-
limit=5
135-
).points
136-
print(f"Found {len(search_result)} results")
137-
except Exception as search_error:
138-
print(f"Search error: {search_error}")
139-
raise HTTPException(status_code=500, detail=f"Search failed: {str(search_error)}")
140-
141-
# Extract content and metadata from search results
142-
docs = []
143-
sources = []
144-
145-
for hit in search_result:
146-
# Debug: print the structure
147-
print(f"Hit payload keys: {hit.payload.keys() if hit.payload else 'No payload'}")
148-
149-
# Try different payload structures
150-
content = ""
151-
metadata = {}
152-
153-
if hit.payload:
154-
# Try direct content access
155-
content = hit.payload.get('page_content', '') or hit.payload.get('content', '')
156-
157-
# Try nested metadata
158-
if 'metadata' in hit.payload:
159-
metadata = hit.payload['metadata']
160-
else:
161-
# Use payload directly as metadata
162-
metadata = hit.payload
163-
164-
if content:
165-
docs.append(content)
166-
167-
# Extract filename from metadata
168-
filename = metadata.get('filename', metadata.get('source', 'Unknown'))
169-
if filename and filename != 'Unknown':
170-
sources.append(filename)
171-
172-
# Create context from documents
173-
context = "\n\n".join([doc for doc in docs if doc.strip()])
174-
175-
# If no context found, provide a helpful message
176-
if not context.strip():
177-
context = "No relevant meeting data found for this query."
178-
179-
# Get history for this session
180134
history = get_history(req.session_id)
181-
history_text = "".join([f"{m['role']}: {m['content']}\n" for m in history[-10:]])
135+
history_text = "".join([f"{m['role']}: {m['content']}\n" for m in history[-6:]])
136+
sources = []
137+
used_chunks = 0
138+
139+
if is_strategic_question(req.message):
140+
# --- STRATEGIC MODE: use master summary ---
141+
print(f"[STRATEGIC] {req.message}")
142+
full_prompt = STRATEGIC_PROMPT.format(
143+
master_summary=MASTER_SUMMARY,
144+
chat_history=history_text,
145+
question=req.message
146+
)
147+
used_chunks = 51 # all batches
148+
else:
149+
# --- SPECIFIC MODE: semantic search ---
150+
print(f"[SPECIFIC] {req.message}")
151+
try:
152+
query_vector = embeddings.embed_query(req.message)
153+
search_result = qdrant_client.query_points(
154+
collection_name=COLLECTION_NAME,
155+
query=query_vector,
156+
limit=10
157+
).points
158+
except Exception as e:
159+
raise HTTPException(status_code=500, detail=f"Search failed: {str(e)}")
160+
161+
docs = []
162+
for hit in search_result:
163+
payload = hit.payload or {}
164+
content = payload.get('page_content', '') or payload.get('content', '')
165+
meta = payload.get('metadata', payload)
166+
if content:
167+
docs.append(content)
168+
filename = meta.get('filename', meta.get('source', ''))
169+
if filename:
170+
sources.append(filename)
171+
172+
context = "\n\n".join([d for d in docs if d.strip()]) or "No relevant meeting data found."
173+
used_chunks = len(docs)
174+
full_prompt = SPECIFIC_PROMPT.format(
175+
context=context,
176+
chat_history=history_text,
177+
question=req.message
178+
)
182179

183-
# Create the prompt
184-
full_prompt = SALES_PROMPT_TEMPLATE.format(
185-
context=context,
186-
chat_history=history_text,
187-
question=req.message
188-
)
189-
190-
# Get response from LLM
191-
print("Sending to LLM...")
192180
try:
193181
response = llm.invoke(full_prompt).content
194-
print("LLM response received")
195-
except Exception as llm_error:
196-
print(f"LLM error: {llm_error}")
197-
response = f"I found {len(docs)} relevant meeting excerpts but encountered an issue. Sources: {', '.join(sources[:3]) if sources else 'your query'}."
182+
except Exception as e:
183+
print(f"LLM error: {e}")
184+
response = "I encountered an issue generating a response. Please try again."
198185

199-
# Save to history
200-
history = get_history(req.session_id)
201186
history.append({"role": "Human", "content": req.message})
202187
history.append({"role": "Assistant", "content": response})
203-
204-
# Remove duplicate sources
205-
sources = list(set(sources))
206-
207-
latency_ms = int((time.time() - start_time) * 1000)
208-
188+
209189
return ChatResp(
210190
answer=response,
211-
sources=sources,
212-
used_chunks=len(docs),
213-
latency_ms=latency_ms
191+
sources=list(set(sources)),
192+
used_chunks=used_chunks,
193+
latency_ms=int((time.time() - start_time) * 1000)
214194
)
215-
195+
216196
except Exception as e:
217-
print(f"Chat error: {str(e)}") # Debug logging
197+
print(f"Chat error: {str(e)}")
218198
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
219199

220200
@app.get("/health")

generate_master_summary.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import os
2+
from pathlib import Path
3+
from openai import OpenAI
4+
from dotenv import load_dotenv
5+
6+
load_dotenv()
7+
8+
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
9+
10+
CLEANED_DIR = "data/clean/cleaned"
11+
OUTPUT_FILE = "data/clean/master_summary.txt"
12+
BATCH_SIZE = 5 # files per batch
13+
14+
BATCH_PROMPT = """You are analyzing a batch of sales meeting transcripts for Enatega (a food delivery platform solution).
15+
Extract and summarize the following from these meetings:
16+
17+
1. Client objections and concerns raised (and how they were handled)
18+
2. Pricing discussions, budget concerns, preferred pricing models
19+
3. Feature requests and product feedback
20+
4. Competitor mentions and comparisons
21+
5. Client industries, regions, and business types
22+
6. What convinced clients to move forward (or not)
23+
7. Common questions asked by clients
24+
8. Integration requirements (POS, payment gateways, etc.)
25+
9. Sales cycle patterns (timeline, decision makers)
26+
10. Any red flags or deal breakers mentioned
27+
28+
Be specific and factual. Reference client names where relevant.
29+
30+
Meetings:
31+
{content}"""
32+
33+
34+
def read_files_in_batches(cleaned_dir: str, batch_size: int):
35+
files = sorted(Path(cleaned_dir).glob("*.txt"))
36+
print(f"Total files: {len(files)}")
37+
for i in range(0, len(files), batch_size):
38+
yield files[i:i + batch_size]
39+
40+
41+
def summarize_batch(files: list, batch_num: int) -> str:
42+
combined = ""
43+
for f in files:
44+
content = f.read_text(encoding="utf-8").strip()
45+
combined += f"\n\n--- {f.stem} ---\n{content}"
46+
47+
print(f" Summarizing batch {batch_num} ({len(files)} files)...")
48+
response = client.chat.completions.create(
49+
model="gpt-4o-mini",
50+
messages=[
51+
{"role": "user", "content": BATCH_PROMPT.format(content=combined)}
52+
],
53+
temperature=0.2,
54+
max_tokens=4000
55+
)
56+
return response.choices[0].message.content.strip()
57+
58+
59+
def generate_master_summary():
60+
output_path = Path(OUTPUT_FILE)
61+
62+
print("Step 1: Summarizing files in batches...\n")
63+
batch_summaries = []
64+
for batch_num, batch_files in enumerate(read_files_in_batches(CLEANED_DIR, BATCH_SIZE), 1):
65+
summary = summarize_batch(batch_files, batch_num)
66+
batch_summaries.append(f"=== BATCH {batch_num} ===\n{summary}")
67+
print(f" ✅ Batch {batch_num} done")
68+
69+
print(f"\nStep 2: Combining {len(batch_summaries)} batch summaries into master knowledge base...")
70+
master_summary = "\n\n".join(batch_summaries)
71+
72+
output_path.parent.mkdir(parents=True, exist_ok=True)
73+
output_path.write_text(master_summary, encoding="utf-8")
74+
print(f"\n✅ Master summary saved to: {OUTPUT_FILE}")
75+
print(f" Length: {len(master_summary)} characters")
76+
print(f" Batches: {len(batch_summaries)}")
77+
78+
79+
if __name__ == "__main__":
80+
generate_master_summary()

0 commit comments

Comments
 (0)