vip11017 commited on
Commit
1fdc232
·
1 Parent(s): ed0b33e

added demo_config functionality

Browse files
app/chatbot/demo_rag.py ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ from typing import List, TypedDict
3
+
4
+ from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
5
+ from langchain_core.tools import tool
6
+ from langchain_core.runnables import RunnableLambda
7
+
8
+ from langchain_qdrant import QdrantVectorStore
9
+ from langchain_huggingface import HuggingFaceEmbeddings
10
+ from langgraph.graph import StateGraph, END
11
+ from langchain_mistralai import ChatMistralAI
12
+
13
+ import time
14
+ import os
15
+ from dotenv import load_dotenv
16
+
17
+ from qdrant_client import QdrantClient
18
+
19
+ from app.mongodb import log_chat
20
+
21
+ load_dotenv()
22
+ os.environ["TOKENIZERS_PARALLELISM"] = "false"
23
+
24
+
25
+ session_histories: dict[str, list] = {}
26
+
27
+ LLM_MODEL = "mistral-medium-latest"
28
+ OPENROUTER_API_KEY = os.getenv('OPENROUTER_API_KEY')
29
+ COLLECTION_NAME = "chatbot_context"
30
+ EMBEDDING_MODEL = "intfloat/e5-base-v2"
31
+ QDRANT_URL = os.getenv('QDRANT_URL')
32
+ QDRANT_API_KEY = os.getenv('QDRANT_API_KEY')
33
+ SUPABASE_URL = os.getenv('SUPABASE_URL')
34
+ SUPABASE_KEY = os.getenv('SUPABASE_KEY')
35
+ MISTRAL_API_KEY = os.getenv('MISTRAL_API_KEY')
36
+
37
+ FAQ_COLLECTION = "auro_faqs"
38
+ BLOGS_COLLECTION = "auro_blogs"
39
+ TECHNOLOGY_COLLECTION = "auro_technology"
40
+ REVOLUTION_COLLECTION = "auro_revolution"
41
+ SUPPORT_COLLECTION = "auro_support"
42
+ PRODUCT_COLLECTION = "auro_product"
43
+
44
+
45
+
46
+ llm = ChatMistralAI(
47
+ model_name=LLM_MODEL,
48
+ api_key=MISTRAL_API_KEY,
49
+ )
50
+
51
+ embeddings = HuggingFaceEmbeddings(model_name=EMBEDDING_MODEL)
52
+
53
+ try:
54
+ client = QdrantClient(url=QDRANT_URL, api_key=QDRANT_API_KEY)
55
+ print(f"Qdrant Collections: {client.get_collections()}")
56
+ except Exception as e:
57
+ raise RuntimeError(f"Failed to connect to Qdrant: {e}")
58
+
59
+
60
+ class GraphState(TypedDict):
61
+ """
62
+ Represents the state of a chat session, including input, output, history, memory,
63
+ response, tool results, and user role for LangGraph
64
+ """
65
+ input: str
66
+ history: List[BaseMessage] #list of past messages
67
+ response: str
68
+ tool_results: dict
69
+ prompt: str
70
+
71
+ from pydantic import BaseModel
72
+
73
+ class ToolInput(BaseModel):
74
+ prompt: str
75
+ iteration: int = 1
76
+
77
+
78
+
79
+
80
+ all_tools = []
81
+
82
+
83
+ def retrieve_node(state: GraphState) -> GraphState:
84
+ query = state['input']
85
+ tool_results = {}
86
+
87
+ for tool in all_tools:
88
+ print(f"Invoking tool: {tool.name} with query: {query}")
89
+ try:
90
+ tool_results[tool.name] = tool.invoke({'query': query})
91
+ print(f"{tool.name} returned {len(tool_results[tool.name])} result(s)")
92
+ except Exception as e:
93
+ tool_results[tool.name] = [{'content': f"Tool {tool.name} failed: {str(e)}", "source": "system"}]
94
+
95
+ state['tool_results'] = tool_results
96
+ return state
97
+
98
+
99
+ def generate_answer(state: GraphState):
100
+ """
101
+ This function generates an answer to the query using the llm and the context provided.
102
+ """
103
+ query = state['input']
104
+
105
+ history = state.get('history', [])
106
+ history_text = "\n".join(
107
+ f"Human: {m.content}" if isinstance(m, HumanMessage) else f"AI: {m.content}"
108
+ for m in history
109
+ )
110
+
111
+
112
+ intermediate_steps = state.get('tool_results', {})
113
+
114
+ steps_string = "\n".join(
115
+ f"{tool_name} Results:\n" +
116
+ "\n".join(
117
+ f"- Product: {entry.get('metadata', {}).get('product_name', 'N/A')}\n {entry['content']}"
118
+ for entry in results
119
+ )
120
+ for tool_name, results in intermediate_steps.items() if results
121
+ )
122
+
123
+
124
+ prompt_input = template.format(
125
+ input=query,
126
+ agent_scratchpad=steps_string,
127
+ history=history_text
128
+ )
129
+
130
+ print(prompt_input)
131
+ state['prompt'] = prompt_input
132
+
133
+ llm_response = llm.invoke(prompt_input)
134
+ state['response'] = llm_response.content if hasattr(llm_response, 'content') else str(llm_response)
135
+ state['history'].append(HumanMessage(content=query))
136
+ state['history'].append(AIMessage(content=state['response']))
137
+
138
+ return state
139
+
140
+
141
+ graph = StateGraph(GraphState)
142
+
143
+ #Add nodes to the graph
144
+ graph.add_node("route_tool", RunnableLambda(retrieve_node))
145
+ graph.add_node("generate_response", RunnableLambda(generate_answer))
146
+
147
+ # Define the flow of the graph
148
+ graph.set_entry_point("route_tool")
149
+ graph.add_edge("route_tool", "generate_response")
150
+ graph.add_edge("generate_response", END)
151
+
152
+ app = graph.compile()
153
+
154
+ async def get_response(query: str, name, email, config) -> dict:
155
+ start_time = time.time()
156
+ session_id = config['configurable']['thread_id']
157
+ history = session_histories.get(session_id, [])
158
+ input_data = {
159
+ "input": query,
160
+ "history": history
161
+ }
162
+ metadata={}
163
+ latency_ms = None
164
+ try:
165
+ result = await app.ainvoke(input_data, config=config)
166
+ latency_ms = int((time.time() - start_time) * 1000)
167
+ session_histories[session_id] = result.get("history", [])
168
+
169
+ metadata = {
170
+ "retrieved_docs": result.get("tool_results", {}),
171
+ "model": LLM_MODEL,
172
+ "embedding_model": EMBEDDING_MODEL,
173
+ "prompt": result.get("prompt", "")
174
+ }
175
+ filtered_result = result['response'].replace("transdermal", "topical")
176
+ result['response'] = filtered_result
177
+ except Exception as e:
178
+ result = {}
179
+ result['response'] = f"Error in processing chat: {e}"
180
+
181
+ print(f"Response: {result['response']}")
182
+
183
+ log_chat(
184
+ session_id=session_id,
185
+ name=name,
186
+ email=email,
187
+ query=query,
188
+ answer=result.get("response", ""),
189
+ latency_ms= latency_ms,
190
+ metadata=metadata
191
+ )
192
+
193
+ return result
app/chatbot/demo_routes.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter
2
+ from pydantic import BaseModel
3
+ from app.chatbot.demo_rag import get_response
4
+
5
+ router = APIRouter()
6
+
7
+ class ChatInput(BaseModel):
8
+ chatbot_id: str
9
+ question: str
10
+ session_id: str
11
+ name: str
12
+ email: str
13
+
14
+ @router.post("/demochat")
15
+ async def demo_chat(input: ChatInput):
16
+ response = await get_response(
17
+ chatbot_id=input.chatbot_id,
18
+ query=input.question,
19
+ session_id=input.session_id,
20
+ name=input.name,
21
+ email=input.email
22
+ )
23
+ return {"answer": response['response']}
app/config.py CHANGED
@@ -30,6 +30,7 @@ try:
30
 
31
  demo_database = client["Demo"]
32
  demo_form_submissions = demo_database["demo_form_submissions"]
 
33
  print("Connected to MongoDB collection successfully!")
34
  except Exception as e:
35
  print(e)
 
30
 
31
  demo_database = client["Demo"]
32
  demo_form_submissions = demo_database["demo_form_submissions"]
33
+ demo_chatbot_configs = demo_database["demo_chatbot_config"]
34
  print("Connected to MongoDB collection successfully!")
35
  except Exception as e:
36
  print(e)
app/ingestion/rag_setup.py CHANGED
@@ -6,7 +6,7 @@ from collections import deque
6
  import tldextract
7
  from typing import List, Dict
8
 
9
- from app.config import qdrant_client, embedding_model
10
 
11
  from qdrant_client.models import VectorParams, Distance
12
 
@@ -16,6 +16,8 @@ from langchain_text_splitters import RecursiveCharacterTextSplitter
16
  from langchain_huggingface import HuggingFaceEmbeddings
17
  from qdrant_client import QdrantClient
18
 
 
 
19
 
20
  def scrape_website(
21
  start_url: str,
@@ -92,7 +94,7 @@ def scrape_website(
92
  return results
93
 
94
 
95
- def chunk_and_embed(chatbot_id: str, pages: List[Dict[str, str]], qdrant_client: QdrantClient):
96
  """
97
  Converts scraped website pages into embedded chunks and stores them in a chabot-scoped Qdrant Collection
98
  """
@@ -146,4 +148,51 @@ def chunk_and_embed(chatbot_id: str, pages: List[Dict[str, str]], qdrant_client:
146
  ids = [str(uuid4()) for _ in chunks]
147
  vector_store.add_documents(chunks, ids=ids)
148
 
149
- print(f"Stored {len(chunks)} chunks in Qdrant collection {collection_name}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  import tldextract
7
  from typing import List, Dict
8
 
9
+ from app.config import qdrant_client, embedding_model, demo_chatbot_configs
10
 
11
  from qdrant_client.models import VectorParams, Distance
12
 
 
16
  from langchain_huggingface import HuggingFaceEmbeddings
17
  from qdrant_client import QdrantClient
18
 
19
+ from app.ingestion.models import ChatbotIngest
20
+
21
 
22
  def scrape_website(
23
  start_url: str,
 
94
  return results
95
 
96
 
97
+ def chunk_and_embed(chatbot_id: str, pages: List[Dict[str, str]]):
98
  """
99
  Converts scraped website pages into embedded chunks and stores them in a chabot-scoped Qdrant Collection
100
  """
 
148
  ids = [str(uuid4()) for _ in chunks]
149
  vector_store.add_documents(chunks, ids=ids)
150
 
151
+ print(f"Stored {len(chunks)} chunks in Qdrant collection {collection_name}")
152
+
153
+
154
+ def build_demo_prompt(ingest: ChatbotIngest) -> str:
155
+ chatbot_name = ingest.chatbot_name or f"{ingest.company_name} Assistant"
156
+ company_name = ingest.company_name
157
+ allowed_topics = ", ".join(ingest.chatbot_purpose) or "general questions"
158
+ banned_topics = ingest.sensitive_topics or "sensitive topics"
159
+ response_style = ", ".join(ingest.tone_style) if ingest.tone_style else "clear and concise"
160
+ fallback_message = f"Sorry, I cannot answer that question. Please contact {ingest.contact_email} or call {ingest.contact_phone}."
161
+ additional_content = "\n".join(ingest.additional_content) if ingest.additional_content else ""
162
+
163
+ template = f"""
164
+ You are {chatbot_name}, an assistant for {company_name}.
165
+ Answer ONLY using the provided context from {company_name}'s approved content.
166
+
167
+ STRICT RULES:
168
+ 1. If the Contextual Knowledge section is empty, say: "{fallback_message}"
169
+ 2. Do NOT use your own general knowledge. Only reference the Contextual Knowledge.
170
+ 3. Only reference topics explicitly allowed: {allowed_topics}.
171
+ 4. Do NOT discuss banned topics: {banned_topics}.
172
+ 5. Keep responses {response_style}.
173
+ 6. Additional context to consider: {additional_content}
174
+ """
175
+ return template
176
+
177
+ def store_demo_rag_config(chatbot_id, ingest: ChatbotIngest) -> None:
178
+ """
179
+ Stores the RAG configuration prompt for the demo chatbot in MongoDB.
180
+ """
181
+ demo_rag_dict = {
182
+ "chatbot_id": chatbot_id,
183
+ "company_name": ingest.company_name,
184
+ "prompt_template": build_demo_prompt(ingest),
185
+ "retrievers": [
186
+ {
187
+ "name": "all",
188
+ "collection": f"chatbot_{chatbot_id}",
189
+ "top_k": 25
190
+ }
191
+ ]
192
+ }
193
+ result = demo_chatbot_configs.insert_one(demo_rag_dict)
194
+ print(f"Inserted RAG config for {ingest.company_name}, _id={result.inserted_id}")
195
+
196
+
197
+
198
+
app/ingestion/workers.py CHANGED
@@ -1,8 +1,7 @@
1
  # workers.py
2
  from app.ingestion.demo_form_fetch_store import get_chatbot_config
3
- from app.ingestion.rag_setup import scrape_website
4
- from app.ingestion.rag_setup import chunk_and_embed
5
- from app.config import qdrant_client
6
 
7
 
8
  def build_rag_for_chatbot(chatbot_id: str) -> None:
@@ -19,5 +18,5 @@ def build_rag_for_chatbot(chatbot_id: str) -> None:
19
  chunk_and_embed(
20
  chatbot_id=chatbot_id,
21
  pages=pages,
22
- qdrant_client=qdrant_client,
23
  )
 
 
1
  # workers.py
2
  from app.ingestion.demo_form_fetch_store import get_chatbot_config
3
+ from app.ingestion.models import ChatbotIngest
4
+ from app.ingestion.rag_setup import scrape_website, chunk_and_embed, store_demo_rag_config
 
5
 
6
 
7
  def build_rag_for_chatbot(chatbot_id: str) -> None:
 
18
  chunk_and_embed(
19
  chatbot_id=chatbot_id,
20
  pages=pages,
 
21
  )
22
+ store_demo_rag_config(chatbot_id=chatbot_id, ingest=ChatbotIngest(**config))
app/main.py CHANGED
@@ -1,5 +1,6 @@
1
  from fastapi import FastAPI
2
  from app.ingestion.routes import router as ingestion_router
 
3
 
4
  app = FastAPI(
5
  title="Chatbot Platform - Demo Ingestion",
@@ -7,5 +8,5 @@ app = FastAPI(
7
  version="1.0.0"
8
  )
9
 
10
- # Mount ingestion routes
11
  app.include_router(ingestion_router, prefix="/ingestion", tags=["ingestion"])
 
 
1
  from fastapi import FastAPI
2
  from app.ingestion.routes import router as ingestion_router
3
+ from app.chatbot.demo_routes import router as demo_router
4
 
5
  app = FastAPI(
6
  title="Chatbot Platform - Demo Ingestion",
 
8
  version="1.0.0"
9
  )
10
 
 
11
  app.include_router(ingestion_router, prefix="/ingestion", tags=["ingestion"])
12
+ app.include_router(demo_router, prefix="/demochatbot", tags=["demochatbot"])