Update agent.py
Browse files
agent.py
CHANGED
@@ -1,94 +1,203 @@
|
|
1 |
-
|
2 |
-
from
|
|
|
|
|
|
|
|
|
|
|
3 |
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
4 |
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
|
5 |
-
from langchain_community.chat_message_histories import ChatMessageHistory #
|
6 |
|
7 |
from tools import (
|
8 |
-
|
|
|
9 |
)
|
10 |
from config.settings import settings
|
11 |
from services.logger import app_logger
|
12 |
|
13 |
-
# Initialize LLM
|
14 |
-
|
15 |
-
#
|
16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
|
18 |
|
19 |
-
# Initialize Tools
|
|
|
20 |
tools = [
|
21 |
UMLSLookupTool(),
|
22 |
BioPortalLookupTool(),
|
23 |
QuantumTreatmentOptimizerTool(),
|
24 |
-
# GeminiTool(), # Add if you want the agent to be able to call Gemini as a sub-LLM.
|
25 |
-
# Be mindful of costs and latency.
|
26 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
27 |
|
28 |
-
# Agent Prompt
|
29 |
-
# You can pull a prompt from Langchain Hub or define your own
|
30 |
-
# e.g., prompt = hub.pull("hwchase17/openai-functions-agent")
|
31 |
prompt = ChatPromptTemplate.from_messages([
|
32 |
-
("system",
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
"Always cite the tool you used if its output is part of your response. "
|
37 |
-
"If asked about treatment for a specific patient, you MUST use the 'quantum_treatment_optimizer' tool. "
|
38 |
-
"Do not provide medical advice directly without tool usage for specific patient cases. "
|
39 |
-
"For general medical knowledge, you can answer directly or use UMLS/BioPortal for definitions and codes."
|
40 |
-
)),
|
41 |
-
MessagesPlaceholder(variable_name="chat_history"),
|
42 |
-
("human", "{input}"),
|
43 |
-
MessagesPlaceholder(variable_name="agent_scratchpad"),
|
44 |
])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
45 |
|
46 |
-
# Create Agent
|
47 |
-
# This agent is optimized for OpenAI function calling.
|
48 |
-
agent = create_openai_functions_agent(llm, tools, prompt)
|
49 |
|
50 |
-
# Create Agent
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
51 |
agent_executor = AgentExecutor(
|
52 |
agent=agent,
|
53 |
tools=tools,
|
54 |
-
verbose=True,
|
55 |
-
handle_parsing_errors=True, #
|
56 |
-
#
|
|
|
|
|
57 |
)
|
|
|
58 |
|
|
|
|
|
59 |
def get_agent_executor():
|
60 |
-
"""Returns the configured agent executor."""
|
61 |
-
|
62 |
-
|
63 |
-
|
|
|
|
|
64 |
return agent_executor
|
65 |
|
66 |
-
# Example
|
|
|
|
|
67 |
if __name__ == "__main__":
|
68 |
-
if not settings.
|
69 |
-
print("Please set your
|
70 |
else:
|
|
|
71 |
executor = get_agent_executor()
|
72 |
-
chat_history = [] # In a real app, this comes from DB or session state
|
73 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
74 |
while True:
|
75 |
user_input = input("You: ")
|
76 |
if user_input.lower() in ["exit", "quit"]:
|
77 |
break
|
78 |
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
"
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
|
|
|
1 |
+
# /home/user/app/agent.py
|
2 |
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
3 |
+
from langchain.agents import AgentExecutor, create_structured_chat_agent # Using a more general agent
|
4 |
+
# If you want to try Gemini's native function calling (experimental and might require specific model versions):
|
5 |
+
# from langchain_google_genai.chat_models import GChatVertexAI # For Vertex AI
|
6 |
+
# from langchain_google_genai import HarmBlockThreshold, HarmCategory # For safety settings
|
7 |
+
|
8 |
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
9 |
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
|
10 |
+
# from langchain_community.chat_message_histories import ChatMessageHistory # Not used directly here
|
11 |
|
12 |
from tools import (
|
13 |
+
UMLSLookupTool, BioPortalLookupTool, QuantumTreatmentOptimizerTool
|
14 |
+
# GeminiTool might be redundant if the main LLM is Gemini, unless it's for a different Gemini model/task.
|
15 |
)
|
16 |
from config.settings import settings
|
17 |
from services.logger import app_logger
|
18 |
|
19 |
+
# --- Initialize LLM (Gemini) ---
|
20 |
+
# Ensure GOOGLE_API_KEY is set in your environment, or pass it directly:
|
21 |
+
# api_key=settings.GEMINI_API_KEY (if settings.GEMINI_API_KEY maps to GOOGLE_API_KEY)
|
22 |
+
try:
|
23 |
+
llm = ChatGoogleGenerativeAI(
|
24 |
+
model="gemini-pro", # Or "gemini-1.5-pro-latest" if available and preferred
|
25 |
+
temperature=0.3,
|
26 |
+
# google_api_key=settings.GEMINI_API_KEY, # Explicitly pass if needed
|
27 |
+
# convert_system_message_to_human=True, # Sometimes helpful for models not strictly adhering to system role
|
28 |
+
# safety_settings={ # Optional: configure safety settings
|
29 |
+
# HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,
|
30 |
+
# HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,
|
31 |
+
# }
|
32 |
+
)
|
33 |
+
app_logger.info("ChatGoogleGenerativeAI (Gemini) initialized successfully.")
|
34 |
+
except Exception as e:
|
35 |
+
app_logger.error(f"Failed to initialize ChatGoogleGenerativeAI: {e}", exc_info=True)
|
36 |
+
# Raise an error that can be caught by get_agent_executor to inform the user
|
37 |
+
raise ValueError(f"Gemini LLM initialization failed: {e}. Check API key and configurations.")
|
38 |
|
39 |
|
40 |
+
# --- Initialize Tools ---
|
41 |
+
# Ensure your tools' descriptions are very clear, especially for non-OpenAI function calling agents.
|
42 |
tools = [
|
43 |
UMLSLookupTool(),
|
44 |
BioPortalLookupTool(),
|
45 |
QuantumTreatmentOptimizerTool(),
|
|
|
|
|
46 |
]
|
47 |
+
app_logger.info(f"Tools initialized: {[tool.name for tool in tools]}")
|
48 |
+
|
49 |
+
|
50 |
+
# --- Agent Prompt (Adapted for a general structured chat agent) ---
|
51 |
+
# This prompt needs to guide the LLM to decide on tool use and format tool calls.
|
52 |
+
# It's more complex than relying on native function calling.
|
53 |
+
# We might need to instruct it to output a specific JSON structure for tool calls.
|
54 |
+
# For now, let's try a ReAct-style approach with create_structured_chat_agent.
|
55 |
+
# LangChain Hub has prompts for this: hub.pull("hwchase17/structured-chat-agent")
|
56 |
+
# Or we define a custom one:
|
57 |
+
|
58 |
+
# This is a simplified prompt. For robust tool use with Gemini (without native function calling),
|
59 |
+
# you'd often use a ReAct prompt or a prompt that guides it to output JSON for tool calls.
|
60 |
+
# `create_structured_chat_agent` is designed to work with models that can follow instructions
|
61 |
+
# to produce a structured output, often for tool usage.
|
62 |
+
|
63 |
+
# For Gemini, which now supports tool calling via its API directly (though LangChain integration might vary),
|
64 |
+
# we *could* try to structure the prompt for that if `ChatGoogleGenerativeAI` has good support.
|
65 |
+
# Let's assume a more general structured chat approach first if direct tool calling isn't as smooth
|
66 |
+
# as with OpenAI's function calling agents.
|
67 |
+
|
68 |
+
# If trying Gemini's newer tool calling features with LangChain (ensure your langchain-google-genai is up to date):
|
69 |
+
# You might be able to bind tools directly to the LLM and use a simpler agent structure.
|
70 |
+
# llm_with_tools = llm.bind_tools(tools) # This is the newer LangChain syntax
|
71 |
+
# Then create an agent that leverages this.
|
72 |
+
|
73 |
+
# For now, let's use `create_structured_chat_agent` which is more general.
|
74 |
+
# This prompt is similar to hwchase17/structured-chat-agent on Langsmith Hub
|
75 |
+
SYSTEM_PROMPT_TEXT = (
|
76 |
+
"You are 'Quantum Health Navigator', a helpful AI assistant for healthcare professionals. "
|
77 |
+
"Respond to the human as helpfully and accurately as possible. "
|
78 |
+
"You have access to the following tools:\n\n"
|
79 |
+
"{tools}\n\n" # This will be filled by the agent with tool names and descriptions
|
80 |
+
"To use a tool, you can use the following format:\n\n"
|
81 |
+
"```json\n"
|
82 |
+
"{{\n"
|
83 |
+
' "action": "tool_name",\n'
|
84 |
+
' "action_input": "input_to_tool"\n' # For tools with single string input
|
85 |
+
# Or for tools with structured input (like QuantumTreatmentOptimizerTool):
|
86 |
+
# ' "action_input": {{"arg1": "value1", "arg2": "value2"}}\n'
|
87 |
+
"}}\n"
|
88 |
+
"```\n\n"
|
89 |
+
"If you use a tool, the system will give you the observation from the tool. "
|
90 |
+
"Then you must respond to the human based on this observation and your knowledge. "
|
91 |
+
"If the human asks a question that doesn't require a tool, answer directly. "
|
92 |
+
"When asked about treatment optimization for a specific patient based on provided context, "
|
93 |
+
"you MUST use the 'quantum_treatment_optimizer' tool. "
|
94 |
+
"For general medical knowledge, you can answer directly or use UMLS/BioPortal. "
|
95 |
+
"Always cite the tool you used if its output is part of your final response. "
|
96 |
+
"Do not provide medical advice directly for specific patient cases without using the 'quantum_treatment_optimizer' tool. "
|
97 |
+
"Patient Context for this session (if provided by the user earlier): {patient_context}\n" # Added patient_context
|
98 |
+
"Begin!\n\n"
|
99 |
+
"Previous conversation history:\n"
|
100 |
+
"{chat_history}\n\n"
|
101 |
+
"New human question: {input}\n"
|
102 |
+
"{agent_scratchpad}" # Placeholder for agent's thoughts and tool outputs
|
103 |
+
)
|
104 |
|
|
|
|
|
|
|
105 |
prompt = ChatPromptTemplate.from_messages([
|
106 |
+
("system", SYSTEM_PROMPT_TEXT),
|
107 |
+
# MessagesPlaceholder(variable_name="chat_history"), # chat_history is now part of the system prompt
|
108 |
+
# ("human", "{input}"), # input is now part of the system prompt
|
109 |
+
MessagesPlaceholder(variable_name="agent_scratchpad"), # For structured chat agent, this is important
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
110 |
])
|
111 |
+
# Note: The `create_structured_chat_agent` expects `input` and `chat_history` to be implicitly handled
|
112 |
+
# or passed through `agent_scratchpad` based on how it formats things.
|
113 |
+
# The prompt structure might need adjustment based on the exact agent behavior.
|
114 |
+
# Often, for these agents, you pass "input" and "chat_history" to invoke, and the prompt template variables
|
115 |
+
# are {input}, {chat_history}, {agent_scratchpad}, {tools}, {tool_names}.
|
116 |
+
|
117 |
+
# For create_structured_chat_agent, the prompt should guide the LLM to produce
|
118 |
+
# either a final answer or a JSON blob for a tool call.
|
119 |
+
# The input variables for the prompt are typically 'input', 'chat_history', 'agent_scratchpad', 'tools', 'tool_names'.
|
120 |
+
# Our SYSTEM_PROMPT_TEXT includes these implicitly or explicitly.
|
121 |
|
|
|
|
|
|
|
122 |
|
123 |
+
# --- Create Agent ---
|
124 |
+
try:
|
125 |
+
# `create_structured_chat_agent` is designed for LLMs that can follow complex instructions
|
126 |
+
# and output structured data (like JSON for tool calls) when prompted to do so.
|
127 |
+
agent = create_structured_chat_agent(llm=llm, tools=tools, prompt=prompt)
|
128 |
+
app_logger.info("Structured chat agent created successfully with Gemini.")
|
129 |
+
except Exception as e:
|
130 |
+
app_logger.error(f"Failed to create structured chat agent: {e}", exc_info=True)
|
131 |
+
raise ValueError(f"Agent creation failed: {e}")
|
132 |
+
|
133 |
+
|
134 |
+
# --- Create Agent Executor ---
|
135 |
agent_executor = AgentExecutor(
|
136 |
agent=agent,
|
137 |
tools=tools,
|
138 |
+
verbose=True,
|
139 |
+
handle_parsing_errors=True, # Important for agents that parse LLM output for tool calls
|
140 |
+
# Example: "Could not parse LLM output: `...`" - then it can retry or return this error.
|
141 |
+
# max_iterations=7, # Good to prevent overly long chains
|
142 |
+
# return_intermediate_steps=True # Useful for debugging to see thought process
|
143 |
)
|
144 |
+
app_logger.info("AgentExecutor created successfully.")
|
145 |
|
146 |
+
|
147 |
+
# --- Getter Function for Streamlit App ---
|
148 |
def get_agent_executor():
|
149 |
+
"""Returns the configured agent executor for Gemini."""
|
150 |
+
# The llm and agent_executor are already initialized.
|
151 |
+
# We check for the API key here as a safeguard, though initialization would have failed earlier.
|
152 |
+
if not (settings.GEMINI_API_KEY or os.environ.get("GOOGLE_API_KEY")): # Check both setting and env var
|
153 |
+
app_logger.error("GOOGLE_API_KEY (for Gemini) not set. Agent will not function.")
|
154 |
+
raise ValueError("Google API Key for Gemini not configured. Agent cannot be initialized.")
|
155 |
return agent_executor
|
156 |
|
157 |
+
# --- Example Usage (for local testing) ---
|
158 |
+
import os # For checking GOOGLE_API_KEY from environment
|
159 |
+
|
160 |
if __name__ == "__main__":
|
161 |
+
if not (settings.GEMINI_API_KEY or os.environ.get("GOOGLE_API_KEY")):
|
162 |
+
print("Please set your GOOGLE_API_KEY (for Gemini) in .env file or environment.")
|
163 |
else:
|
164 |
+
print("Gemini Agent Test Console (type 'exit' or 'quit' to stop)")
|
165 |
executor = get_agent_executor()
|
|
|
166 |
|
167 |
+
# For structured chat agents, chat_history is often passed in the invoke call.
|
168 |
+
# The agent prompt includes {chat_history}.
|
169 |
+
current_chat_history = [] # List of HumanMessage, AIMessage
|
170 |
+
|
171 |
+
# Initial patient context (simulated for testing)
|
172 |
+
patient_context_for_test = {
|
173 |
+
"age": 35,
|
174 |
+
"gender": "Male",
|
175 |
+
"key_medical_history": "Type 2 Diabetes, Hypertension",
|
176 |
+
"current_medications": "Metformin, Lisinopril"
|
177 |
+
}
|
178 |
+
context_summary_parts_test = [f"{k.replace('_', ' ').title()}: {v}" for k, v in patient_context_for_test.items() if v]
|
179 |
+
patient_context_str_test = "; ".join(context_summary_parts_test) if context_summary_parts_test else "None provided."
|
180 |
+
|
181 |
+
|
182 |
while True:
|
183 |
user_input = input("You: ")
|
184 |
if user_input.lower() in ["exit", "quit"]:
|
185 |
break
|
186 |
|
187 |
+
try:
|
188 |
+
response = executor.invoke({
|
189 |
+
"input": user_input,
|
190 |
+
"chat_history": current_chat_history,
|
191 |
+
"patient_context": patient_context_str_test # Passing patient context
|
192 |
+
# `tools` and `tool_names` are usually handled by the agent constructor
|
193 |
+
})
|
194 |
+
|
195 |
+
ai_output = response.get('output', "No output from agent.")
|
196 |
+
print(f"Agent: {ai_output}")
|
197 |
+
|
198 |
+
current_chat_history.append(HumanMessage(content=user_input))
|
199 |
+
current_chat_history.append(AIMessage(content=ai_output))
|
200 |
+
|
201 |
+
except Exception as e:
|
202 |
+
print(f"Error invoking agent: {e}")
|
203 |
+
app_logger.error(f"Error in __main__ agent test: {e}", exc_info=True)
|