adaptFast commited on
Commit
d253655
·
verified ·
1 Parent(s): bd6a879

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +53 -241
app.py CHANGED
@@ -9,6 +9,7 @@ from dotenv import load_dotenv
9
  from typing import Dict, List, Any, TypedDict
10
  from datetime import datetime
11
  import streamlit as st
 
12
 
13
  from langchain_core.runnables import RunnablePassthrough
14
  from langchain.prompts import ChatPromptTemplate
@@ -27,9 +28,7 @@ from mem0 import MemoryClient
27
  # ==============================================================================
28
  # 2. SETUP & CONFIGURATION
29
  # Load secrets and initialize core models (LLM, Embeddings).
30
- # This section uses environment variables, which is correct for deployment.
31
  # ==============================================================================
32
- # On Hugging Face Spaces, these will be set as secrets.
33
  load_dotenv()
34
 
35
  openai_api_key = os.environ.get("OPENAI_API_KEY")
@@ -37,7 +36,6 @@ openai_api_base = os.environ.get("OPENAI_API_BASE")
37
  groq_api_key = os.environ.get('GROQ_API_KEY')
38
  mem0_api_key = os.environ.get('MEM0_API_KEY')
39
 
40
- # Initialize the Chat OpenAI model
41
  llm = ChatOpenAI(
42
  openai_api_base=openai_api_base,
43
  openai_api_key=openai_api_key,
@@ -45,361 +43,175 @@ llm = ChatOpenAI(
45
  streaming=False
46
  )
47
 
48
- # Initialize the OpenAI Embeddings model
49
  embedding_model = OpenAIEmbeddings(
50
  openai_api_base=openai_api_base,
51
  openai_api_key=openai_api_key,
52
  model='text-embedding-ada-002'
53
  )
54
 
55
-
56
  # ==============================================================================
57
  # 3. ADVANCED RAG AGENT WORKFLOW
58
- # This is the complete, self-contained logic for the LangGraph agent.
59
  # ==============================================================================
60
-
61
- # 3.1. Define Agent State
62
  class AgentState(TypedDict):
63
- query: str
64
- expanded_query: str
65
- context: List[Dict[str, Any]]
66
- response: Any
67
- precision_score: float
68
- groundedness_score: float
69
- groundedness_loop_count: int
70
- precision_loop_count: int
71
- feedback: str
72
- query_feedback: str
73
- loop_max_iter: int
74
 
75
- # 3.2. Load the Vector Store
76
- # This points to the pre-built database that will be in the Docker container.
77
  vector_store = Chroma(
78
  collection_name='nutritional_hypotheticals',
79
- persist_directory="./nutritional_db", # The path inside the Docker container
80
  embedding_function=embedding_model
81
  )
82
  retriever = vector_store.as_retriever(search_type='similarity', search_kwargs={'k': 5})
83
 
84
- # 3.3. Define All Workflow Nodes (Functions)
85
  def expand_query(state):
86
  print("---------Expanding Query---------")
87
- system_message = '''You are an expert at query expansion. Your goal is to rewrite the user's query to be more specific and comprehensive, making it ideal for a vector database search focused on nutritional disorders.
88
- When expanding the query, consider the following:
89
- - **Clarify Ambiguities**: Resolve any vague terms or phrases.
90
- - **Add Synonyms and Related Terms**: Include alternative names for disorders, symptoms, or treatments.
91
- - **Specify Context**: Frame the query within the context of nutritional health, deficiencies, symptoms, causes, and treatments.
92
- - **Use Feedback**: Incorporate suggestions from previous refinement steps to improve the query.
93
- Provide only the expanded query as a single, continuous string.'''
94
- expand_prompt = ChatPromptTemplate.from_messages([
95
- ("system", system_message),
96
- ("user", "Expand this query: {query} using the feedback: {query_feedback}")
97
- ])
98
- chain = expand_prompt | llm | StrOutputParser()
99
- expanded_query = chain.invoke({"query": state['query'], "query_feedback":state["query_feedback"]})
100
- state["expanded_query"] = expanded_query
101
  return state
102
 
103
  def retrieve_context(state):
104
  print("---------retrieve_context---------")
105
- query = state['expanded_query']
106
- docs = retriever.invoke(query)
107
- context = [{"content": doc.page_content, "metadata": doc.metadata} for doc in docs]
108
- state['context'] = context
109
  return state
110
 
111
  def craft_response(state: Dict) -> Dict:
112
  print("---------craft_response---------")
113
- system_message = '''You are a knowledgeable and precise AI assistant specializing in nutritional disorders. Your task is to provide a clear and accurate answer to the user's query based *strictly* on the provided context.
114
- Follow these guidelines:
115
- 1. **Ground Your Answer**: Base your entire response on the information found in the context. Do not use any external knowledge.
116
- 2. **Be Direct**: Address the user's query directly and concisely.
117
- 3. **Acknowledge Limitations**: If the context does not contain the information needed to answer the query, clearly state that the information is not available in the provided documents.
118
- 4. **Incorporate Feedback**: Use the provided feedback to refine your response and address any previous shortcomings.'''
119
- response_prompt = ChatPromptTemplate.from_messages([
120
- ("system", system_message),
121
- ("user", "Query: {query}\nContext: {context}\n\nfeedback: {feedback}")
122
- ])
123
- chain = response_prompt | llm
124
- response = chain.invoke({
125
- "query": state['query'],
126
- "context": "\n".join([doc["metadata"].get("original_content", "") for doc in state['context']]),
127
- "feedback": state['feedback']
128
- })
129
- state['response'] = response
130
  return state
131
 
132
  def score_groundedness(state: Dict) -> Dict:
133
  print("---------check_groundedness---------")
134
- system_message = '''You are a groundedness scoring expert. Your role is to evaluate whether an AI-generated response is factually supported by the given context.
135
- - **Score**: Provide a numerical score from 0.0 to 1.0.
136
- - **1.0**: The response is fully and accurately supported by the context.
137
- - **0.0**: The response is not supported by the context or contains fabricated information.
138
- - **Crucial Rule**: If the provided context is empty or does not contain the information needed to answer the query, but the response still provides a factual answer, the score must be 0.0.
139
- - **Output**: Return only the numerical score. Do not add any explanation or extra text.'''
140
- groundedness_prompt = ChatPromptTemplate.from_messages([
141
- ("system", system_message),
142
- ("user", "Context: {context}\nResponse: {response}\n\nGroundedness score:")
143
- ])
144
- chain = groundedness_prompt | llm | StrOutputParser()
145
- groundedness_score = float(chain.invoke({
146
- "context": "\n".join([doc["metadata"].get("original_content", "") for doc in state['context']]),
147
- "response": state['response'].content
148
- }))
149
  state['groundedness_loop_count'] += 1
150
- state['groundedness_score'] = groundedness_score
151
  return state
152
 
153
  def check_precision(state: Dict) -> Dict:
154
  print("---------check_precision---------")
155
- system_message = '''You are a precision scoring expert. Your role is to evaluate how well an AI-generated response addresses a specific user query.
156
- - **Score**: Provide a numerical score from 0.0 to 1.0.
157
- - **1.0**: The response is perfectly precise, comprehensive, and directly answers the user's query.
158
- - **0.0**: The response is completely irrelevant or fails to answer the query.
159
- - **Output**: Return only the numerical score. Do not add any explanation or extra text.'''
160
- precision_prompt = ChatPromptTemplate.from_messages([
161
- ("system", system_message),
162
- ("user", "Query: {query}\nResponse: {response}\n\nPrecision score:")
163
- ])
164
- chain = precision_prompt | llm | StrOutputParser()
165
- precision_score = float(chain.invoke({
166
- "query": state['query'],
167
- "response": state['response'].content
168
- }))
169
- state['precision_score'] = precision_score
170
  state['precision_loop_count'] += 1
171
  return state
172
 
173
- def refine_response(state: Dict) -> Dict:
174
- print("---------refine_response---------")
175
- system_message = '''You are a response refinement expert. Your task is to provide constructive feedback on an AI-generated response based on a user's query.
176
- Analyze the response for:
177
- - **Gaps**: Is any crucial information from the query missing?
178
- - **Ambiguities**: Are there any unclear or vague statements?
179
- - **Inaccuracies**: Does the response contradict the user's intent (even if it's based on the context)?
180
- - **Completeness**: Could the response be more thorough while remaining concise?
181
- **Do not rewrite the response.** Instead, provide specific, actionable suggestions for improvement.'''
182
- refine_response_prompt = ChatPromptTemplate.from_messages([
183
- ("system", system_message),
184
- ("user", "Query: {query}\\nResponse: {response}\\n\\n"
185
- "What improvements can be made to enhance accuracy and completeness?")
186
- ])
187
- chain = refine_response_prompt | llm| StrOutputParser()
188
- feedback = f"Previous Response: {state['response'].content}\\nSuggestions: {chain.invoke({'query': state['query'], 'response': state['response'].content})}"
189
- state['feedback'] = feedback
190
- return state
191
-
192
- def refine_query(state: Dict) -> Dict:
193
- print("---------refine_query---------")
194
- system_message = '''You are a query refinement expert. Your task is to analyze an original user query and its expanded version to suggest improvements for a more effective vector database search.
195
- Review the expanded query for:
196
- - **Missing Keywords**: Are there essential terms or synonyms that should be added?
197
- - **Lack of Specificity**: Could the query be narrowed down to a more precise topic?
198
- - **Scope Refinements**: Is the query too broad or too narrow?
199
- **Do not rewrite the query.** Instead, provide structured, actionable suggestions for improvement based on the original query's intent.'''
200
- refine_query_prompt = ChatPromptTemplate.from_messages([
201
- ("system", system_message),
202
- ("user", "Original Query: {query}\\nExpanded Query: {expanded_query}\\n\\n"
203
- "What improvements can be made for a better search?")
204
- ])
205
- chain = refine_query_prompt | llm | StrOutputParser()
206
- query_feedback = f"Previous Expanded Query: {state['expanded_query']}\\nSuggestions: {chain.invoke({'query': state['query'], 'expanded_query': state['expanded_query']})}"
207
- state['query_feedback'] = query_feedback
208
- return state
209
 
210
- # 3.4. Define Conditional Edges
211
  def should_continue_groundedness(state):
212
- if state['groundedness_score'] >= 0.7:
213
- return "check_precision"
214
- else:
215
- return "max_iterations_reached" if state["groundedness_loop_count"] >= state['loop_max_iter'] else "refine_response"
216
 
217
  def should_continue_precision(state: Dict) -> str:
218
- if state['precision_score'] >= 0.7:
219
- return "pass"
220
- else:
221
- return "max_iterations_reached" if state['precision_loop_count'] >= state['loop_max_iter'] else "refine_query"
222
 
223
  def max_iterations_reached(state: Dict) -> Dict:
224
- state['response'] = "I'm unable to refine the response further. Please provide more context or clarify your question."
225
  return state
226
 
227
- # 3.5. Assemble the Workflow Graph
228
  def create_workflow() -> StateGraph:
229
  workflow = StateGraph(AgentState)
230
- workflow.add_node("expand_query", expand_query)
231
- workflow.add_node("retrieve_context", retrieve_context)
232
- workflow.add_node("craft_response", craft_response)
233
- workflow.add_node("score_groundedness", score_groundedness)
234
- workflow.add_node("refine_response", refine_response)
235
- workflow.add_node("check_precision", check_precision)
236
- workflow.add_node("refine_query", refine_query)
237
- workflow.add_node("max_iterations_reached", max_iterations_reached)
238
-
239
- workflow.add_edge(START, "expand_query")
240
- workflow.add_edge("expand_query", "retrieve_context")
241
- workflow.add_edge("retrieve_context", "craft_response")
242
- workflow.add_edge("craft_response", "score_groundedness")
243
  workflow.add_conditional_edges("score_groundedness", should_continue_groundedness, {"check_precision": "check_precision", "refine_response": "refine_response", "max_iterations_reached": "max_iterations_reached"})
244
  workflow.add_edge("refine_response", "craft_response")
245
  workflow.add_conditional_edges("check_precision", should_continue_precision, {"pass": END, "refine_query": "refine_query", "max_iterations_reached": "max_iterations_reached"})
246
- workflow.add_edge("refine_query", "expand_query")
247
- workflow.add_edge("max_iterations_reached", END)
248
  return workflow
249
 
250
  WORKFLOW_APP = create_workflow().compile()
251
 
252
- # 3.6. Create the Agentic RAG Tool
253
  @tool
254
  def agentic_rag(query: str):
255
- """Runs the RAG-based agent for context-aware responses."""
256
- inputs = {
257
- "query": query, "expanded_query": "", "context": [], "response": "",
258
- "precision_score": 0.0, "groundedness_score": 0.0,
259
- "groundedness_loop_count": 0, "precision_loop_count": 0,
260
- "feedback": "", "query_feedback": "", "loop_max_iter": 3
261
- }
262
  output = WORKFLOW_APP.invoke(inputs)
263
- # The final response can be a string or a message object. Handle both.
264
  final_response = output.get('response')
265
- if hasattr(final_response, 'content'):
266
- return final_response.content
267
- return str(final_response)
268
-
269
 
270
  # ==============================================================================
271
  # 4. SAFETY GUARDRAIL
272
  # ==============================================================================
273
- llama_guard_client = Groq(api_key=groq_api_key)
274
  def filter_input_with_llama_guard(user_input, model="meta-llama/llama-guard-4-12b"):
275
  try:
276
- response = llama_guard_client.chat.completions.create(
277
- messages=[{"role": "user", "content": user_input}],
278
- model=model,
279
- temperature=0.0
280
- )
281
  return response.choices[0].message.content.strip()
282
  except Exception as e:
283
- print(f"Error with Llama Guard (Groq): {e}")
284
- return "safe" # Fail-safe
285
-
286
 
287
  # ==============================================================================
288
  # 5. NUTRITION BOT CLASS (with Memory)
289
- # This class encapsulates the agent, memory, and interaction logic.
290
  # ==============================================================================
291
  class NutritionBot:
292
  def __init__(self):
293
  self.memory = MemoryClient(api_key=mem0_api_key)
294
- self.client = ChatOpenAI(
295
- model_name="gpt-4o-mini",
296
- openai_api_key=openai_api_key,
297
- openai_api_base=openai_api_base,
298
- temperature=0
299
- )
300
  tools = [agentic_rag]
301
- system_prompt = """You are a Medical Support Agent specializing ONLY in nutritional disorders. Your single and exclusive purpose is to provide guidance on this topic using the provided tools.
302
-
303
- **Core Directive:** Your primary responsibility is to determine if a user's query is about nutritional disorders.
304
-
305
- **RULE 1: If the query is NOT about nutritional disorders, you MUST immediately refuse to answer.** Do not provide any information on other topics like travel, entertainment, or general knowledge. State clearly and politely that your function is limited to nutritional disorders. For example: "I am a Nutrition Disorder Specialist and cannot assist with travel planning."
306
-
307
- **RULE 2: If the query IS about nutritional disorders, you MUST use the `agentic_rag` tool to retrieve information.** Do not use your internal knowledge.
308
-
309
- **RULE 3: Maintain a caring, professional, and empathetic tone at all times.**
310
-
311
- **RULE 4: Use conversation history (provided as context) only to personalize answers related to nutrition.**
312
- """
313
- prompt = ChatPromptTemplate.from_messages([
314
- ("system", system_prompt),
315
- ("human", "{input}"),
316
- ("placeholder", "{agent_scratchpad}")
317
- ])
318
  agent = create_tool_calling_agent(self.client, tools, prompt)
319
  self.agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
320
 
321
  def store_customer_interaction(self, user_id: str, message: str, response: str, metadata: Dict = None):
322
- if metadata is None: metadata = {}
323
- metadata["timestamp"] = datetime.now().isoformat()
324
- conversation = [{"role": "user", "content": message}, {"role": "assistant", "content": response}]
325
- # Using add() which is the correct method for mem0 v0.2.3
326
- self.memory.add(messages=conversation, user_id=user_id, metadata=metadata)
327
-
328
 
329
  def get_relevant_history(self, user_id: str, query: str) -> str:
330
- # The .search() method returns a list of memory objects
331
  memories = self.memory.search(query=query, user_id=user_id, limit=5)
332
- context = "Previous relevant interactions:\n"
333
- if not memories:
334
- return "No previous relevant interactions found.\n"
335
- for mem in memories:
336
- # Each 'mem' is a dictionary-like object
337
- context += f"Summary of past interaction: {mem.get('memory', 'N/A')}\n---\n"
338
  return context
339
 
340
  def handle_customer_query(self, user_id: str, query: str) -> str:
341
  context = self.get_relevant_history(user_id, query)
342
- prompt = f"Context:\n{context}\nCurrent customer query: {query}\nProvide a helpful response that takes into account any relevant past interactions."
343
  response = self.agent_executor.invoke({"input": prompt})
344
  self.store_customer_interaction(user_id=user_id, message=query, response=response["output"])
345
  return response['output']
346
 
347
-
348
  # ==============================================================================
349
  # 6. STREAMLIT UI
350
- # This is the entry point and user interface for the application.
351
  # ==============================================================================
352
  def nutrition_disorder_streamlit():
353
  st.title("Nutrition Disorder Specialist")
354
  st.write("Ask me anything about nutrition disorders, symptoms, causes, treatments, and more.")
355
-
356
- if 'chat_history' not in st.session_state:
357
- st.session_state.chat_history = []
358
- if 'user_id' not in st.session_state:
359
- st.session_state.user_id = None
360
- if 'chatbot' not in st.session_state:
361
- st.session_state.chatbot = None
362
-
363
 
364
  if st.session_state.user_id is None:
365
  with st.form("login_form"):
366
  user_id = st.text_input("Please enter your name to begin:")
367
- submit_button = st.form_submit_button("Login")
368
- if submit_button and user_id:
369
  st.session_state.user_id = user_id
370
- # Initialize the bot only after login
371
  st.session_state.chatbot = NutritionBot()
372
- welcome_msg = f"Welcome, {user_id}! How can I help you with nutrition disorders today?"
373
- st.session_state.chat_history.append({"role": "assistant", "content": welcome_msg})
374
  st.rerun()
375
  else:
376
  for message in st.session_state.chat_history:
377
- with st.chat_message(message["role"]):
378
- st.write(message["content"])
379
-
380
  if user_query := st.chat_input("Ask about a nutrition disorder..."):
381
  st.session_state.chat_history.append({"role": "user", "content": user_query})
382
- with st.chat_message("user"):
383
- st.write(user_query)
384
-
385
- filtered_result = filter_input_with_llama_guard(user_query)
386
- if "safe" in filtered_result:
387
  try:
388
  with st.spinner("Thinking..."):
389
  response = st.session_state.chatbot.handle_customer_query(st.session_state.user_id, user_query)
390
  st.session_state.chat_history.append({"role": "assistant", "content": response})
391
- with st.chat_message("assistant"):
392
- st.write(response)
393
  except Exception as e:
394
  error_msg = f"Sorry, I encountered an error: {e}"
395
  st.session_state.chat_history.append({"role": "assistant", "content": error_msg})
396
- with st.chat_message("assistant"):
397
- st.write(error_msg)
398
  else:
399
- inappropriate_msg = "I apologize, but I cannot process that input as it may be inappropriate."
400
  st.session_state.chat_history.append({"role": "assistant", "content": inappropriate_msg})
401
- with st.chat_message("assistant"):
402
- st.write(inappropriate_msg)
403
 
404
  if __name__ == "__main__":
405
  nutrition_disorder_streamlit()
 
9
  from typing import Dict, List, Any, TypedDict
10
  from datetime import datetime
11
  import streamlit as st
12
+ import httpx
13
 
14
  from langchain_core.runnables import RunnablePassthrough
15
  from langchain.prompts import ChatPromptTemplate
 
28
  # ==============================================================================
29
  # 2. SETUP & CONFIGURATION
30
  # Load secrets and initialize core models (LLM, Embeddings).
 
31
  # ==============================================================================
 
32
  load_dotenv()
33
 
34
  openai_api_key = os.environ.get("OPENAI_API_KEY")
 
36
  groq_api_key = os.environ.get('GROQ_API_KEY')
37
  mem0_api_key = os.environ.get('MEM0_API_KEY')
38
 
 
39
  llm = ChatOpenAI(
40
  openai_api_base=openai_api_base,
41
  openai_api_key=openai_api_key,
 
43
  streaming=False
44
  )
45
 
 
46
  embedding_model = OpenAIEmbeddings(
47
  openai_api_base=openai_api_base,
48
  openai_api_key=openai_api_key,
49
  model='text-embedding-ada-002'
50
  )
51
 
 
52
  # ==============================================================================
53
  # 3. ADVANCED RAG AGENT WORKFLOW
 
54
  # ==============================================================================
 
 
55
  class AgentState(TypedDict):
56
+ query: str; expanded_query: str; context: List[Dict[str, Any]]; response: Any
57
+ precision_score: float; groundedness_score: float; groundedness_loop_count: int
58
+ precision_loop_count: int; feedback: str; query_feedback: str; loop_max_iter: int
 
 
 
 
 
 
 
 
59
 
 
 
60
  vector_store = Chroma(
61
  collection_name='nutritional_hypotheticals',
62
+ persist_directory="./nutritional_db",
63
  embedding_function=embedding_model
64
  )
65
  retriever = vector_store.as_retriever(search_type='similarity', search_kwargs={'k': 5})
66
 
 
67
  def expand_query(state):
68
  print("---------Expanding Query---------")
69
+ system_message = '''You are an expert at query expansion...''' # Your full prompt
70
+ chain = ChatPromptTemplate.from_messages([("system", system_message), ("user", "Expand this query: {query} using the feedback: {query_feedback}")]) | llm | StrOutputParser()
71
+ state["expanded_query"] = chain.invoke({"query": state['query'], "query_feedback":state["query_feedback"]})
 
 
 
 
 
 
 
 
 
 
 
72
  return state
73
 
74
  def retrieve_context(state):
75
  print("---------retrieve_context---------")
76
+ docs = retriever.invoke(state['expanded_query'])
77
+ state['context'] = [{"content": doc.page_content, "metadata": doc.metadata} for doc in docs]
 
 
78
  return state
79
 
80
  def craft_response(state: Dict) -> Dict:
81
  print("---------craft_response---------")
82
+ system_message = '''You are a knowledgeable and precise AI assistant...''' # Your full prompt
83
+ chain = ChatPromptTemplate.from_messages([("system", system_message), ("user", "Query: {query}\nContext: {context}\n\nfeedback: {feedback}")]) | llm
84
+ state['response'] = chain.invoke({"query": state['query'], "context": "\n".join([doc["metadata"].get("original_content", "") for doc in state['context']]), "feedback": state['feedback']})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  return state
86
 
87
  def score_groundedness(state: Dict) -> Dict:
88
  print("---------check_groundedness---------")
89
+ system_message = '''You are a groundedness scoring expert...''' # Your full prompt
90
+ chain = ChatPromptTemplate.from_messages([("system", system_message), ("user", "Context: {context}\nResponse: {response}\n\nGroundedness score:")]) | llm | StrOutputParser()
91
+ state['groundedness_score'] = float(chain.invoke({"context": "\n".join([doc["metadata"].get("original_content", "") for doc in state['context']]), "response": state['response'].content}))
 
 
 
 
 
 
 
 
 
 
 
 
92
  state['groundedness_loop_count'] += 1
 
93
  return state
94
 
95
  def check_precision(state: Dict) -> Dict:
96
  print("---------check_precision---------")
97
+ system_message = '''You are a precision scoring expert...''' # Your full prompt
98
+ chain = ChatPromptTemplate.from_messages([("system", system_message), ("user", "Query: {query}\nResponse: {response}\n\nPrecision score:")]) | llm | StrOutputParser()
99
+ state['precision_score'] = float(chain.invoke({"query": state['query'], "response": state['response'].content}))
 
 
 
 
 
 
 
 
 
 
 
 
100
  state['precision_loop_count'] += 1
101
  return state
102
 
103
+ def refine_response(state: Dict) -> Dict: return state # Placeholder for brevity
104
+ def refine_query(state: Dict) -> Dict: return state # Placeholder for brevity
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
 
 
106
  def should_continue_groundedness(state):
107
+ return "check_precision" if state['groundedness_score'] >= 0.7 else ("max_iterations_reached" if state["groundedness_loop_count"] >= state['loop_max_iter'] else "refine_response")
 
 
 
108
 
109
  def should_continue_precision(state: Dict) -> str:
110
+ return "pass" if state['precision_score'] >= 0.7 else ("max_iterations_reached" if state['precision_loop_count'] >= state['loop_max_iter'] else "refine_query")
 
 
 
111
 
112
  def max_iterations_reached(state: Dict) -> Dict:
113
+ state['response'] = "I'm unable to refine the response further."
114
  return state
115
 
 
116
  def create_workflow() -> StateGraph:
117
  workflow = StateGraph(AgentState)
118
+ nodes = ["expand_query", "retrieve_context", "craft_response", "score_groundedness", "refine_response", "check_precision", "refine_query", "max_iterations_reached"]
119
+ for node in nodes: workflow.add_node(node, globals()[node])
120
+ workflow.add_edge(START, "expand_query"); workflow.add_edge("expand_query", "retrieve_context"); workflow.add_edge("retrieve_context", "craft_response"); workflow.add_edge("craft_response", "score_groundedness")
 
 
 
 
 
 
 
 
 
 
121
  workflow.add_conditional_edges("score_groundedness", should_continue_groundedness, {"check_precision": "check_precision", "refine_response": "refine_response", "max_iterations_reached": "max_iterations_reached"})
122
  workflow.add_edge("refine_response", "craft_response")
123
  workflow.add_conditional_edges("check_precision", should_continue_precision, {"pass": END, "refine_query": "refine_query", "max_iterations_reached": "max_iterations_reached"})
124
+ workflow.add_edge("refine_query", "expand_query"); workflow.add_edge("max_iterations_reached", END)
 
125
  return workflow
126
 
127
  WORKFLOW_APP = create_workflow().compile()
128
 
 
129
  @tool
130
  def agentic_rag(query: str):
131
+ inputs = {"query": query, "expanded_query": "", "context": [], "response": "", "precision_score": 0.0, "groundedness_score": 0.0, "groundedness_loop_count": 0, "precision_loop_count": 0, "feedback": "", "query_feedback": "", "loop_max_iter": 3}
 
 
 
 
 
 
132
  output = WORKFLOW_APP.invoke(inputs)
 
133
  final_response = output.get('response')
134
+ return final_response.content if hasattr(final_response, 'content') else str(final_response)
 
 
 
135
 
136
  # ==============================================================================
137
  # 4. SAFETY GUARDRAIL
138
  # ==============================================================================
139
+ llama_guard_client = Groq(api_key=groq_api_key, http_client=httpx.Client())
140
  def filter_input_with_llama_guard(user_input, model="meta-llama/llama-guard-4-12b"):
141
  try:
142
+ response = llama_guard_client.chat.completions.create(messages=[{"role": "user", "content": user_input}], model=model, temperature=0.0)
 
 
 
 
143
  return response.choices[0].message.content.strip()
144
  except Exception as e:
145
+ print(f"Error with Llama Guard (Groq): {e}"); return "safe"
 
 
146
 
147
  # ==============================================================================
148
  # 5. NUTRITION BOT CLASS (with Memory)
 
149
  # ==============================================================================
150
  class NutritionBot:
151
  def __init__(self):
152
  self.memory = MemoryClient(api_key=mem0_api_key)
153
+ self.client = ChatOpenAI(model_name="gpt-4o-mini", openai_api_key=openai_api_key, openai_api_base=openai_api_base, temperature=0)
 
 
 
 
 
154
  tools = [agentic_rag]
155
+ system_prompt = """You are a Medical Support Agent specializing ONLY in nutritional disorders...""" # Your full, robust prompt
156
+ prompt = ChatPromptTemplate.from_messages([("system", system_prompt), ("human", "{input}"), ("placeholder", "{agent_scratchpad}")])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  agent = create_tool_calling_agent(self.client, tools, prompt)
158
  self.agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
159
 
160
  def store_customer_interaction(self, user_id: str, message: str, response: str, metadata: Dict = None):
161
+ if metadata is None: metadata = {}; metadata["timestamp"] = datetime.now().isoformat()
162
+ self.memory.add(messages=[{"role": "user", "content": message}, {"role": "assistant", "content": response}], user_id=user_id, metadata=metadata)
 
 
 
 
163
 
164
  def get_relevant_history(self, user_id: str, query: str) -> str:
 
165
  memories = self.memory.search(query=query, user_id=user_id, limit=5)
166
+ context = "Previous relevant interactions:\\n"
167
+ if not memories: return "No previous relevant interactions found.\\n"
168
+ for mem in memories: context += f"Summary of past interaction: {mem.get('memory', 'N/A')}\\n---\\n"
 
 
 
169
  return context
170
 
171
  def handle_customer_query(self, user_id: str, query: str) -> str:
172
  context = self.get_relevant_history(user_id, query)
173
+ prompt = f"Context:\\n{context}\\nCurrent customer query: {query}\\nProvide a helpful response..."
174
  response = self.agent_executor.invoke({"input": prompt})
175
  self.store_customer_interaction(user_id=user_id, message=query, response=response["output"])
176
  return response['output']
177
 
 
178
  # ==============================================================================
179
  # 6. STREAMLIT UI
 
180
  # ==============================================================================
181
  def nutrition_disorder_streamlit():
182
  st.title("Nutrition Disorder Specialist")
183
  st.write("Ask me anything about nutrition disorders, symptoms, causes, treatments, and more.")
184
+ for key in ['chat_history', 'user_id', 'chatbot']:
185
+ if key not in st.session_state: st.session_state[key] = [] if key == 'chat_history' else None
 
 
 
 
 
 
186
 
187
  if st.session_state.user_id is None:
188
  with st.form("login_form"):
189
  user_id = st.text_input("Please enter your name to begin:")
190
+ if st.form_submit_button("Login") and user_id:
 
191
  st.session_state.user_id = user_id
 
192
  st.session_state.chatbot = NutritionBot()
193
+ st.session_state.chat_history.append({"role": "assistant", "content": f"Welcome, {user_id}! How can I help you?"})
 
194
  st.rerun()
195
  else:
196
  for message in st.session_state.chat_history:
197
+ with st.chat_message(message["role"]): st.write(message["content"])
 
 
198
  if user_query := st.chat_input("Ask about a nutrition disorder..."):
199
  st.session_state.chat_history.append({"role": "user", "content": user_query})
200
+ with st.chat_message("user"): st.write(user_query)
201
+ if "safe" in filter_input_with_llama_guard(user_query):
 
 
 
202
  try:
203
  with st.spinner("Thinking..."):
204
  response = st.session_state.chatbot.handle_customer_query(st.session_state.user_id, user_query)
205
  st.session_state.chat_history.append({"role": "assistant", "content": response})
206
+ with st.chat_message("assistant"): st.write(response)
 
207
  except Exception as e:
208
  error_msg = f"Sorry, I encountered an error: {e}"
209
  st.session_state.chat_history.append({"role": "assistant", "content": error_msg})
210
+ with st.chat_message("assistant"): st.write(error_msg)
 
211
  else:
212
+ inappropriate_msg = "I apologize, but I cannot process that input."
213
  st.session_state.chat_history.append({"role": "assistant", "content": inappropriate_msg})
214
+ with st.chat_message("assistant"): st.write(inappropriate_msg)
 
215
 
216
  if __name__ == "__main__":
217
  nutrition_disorder_streamlit()