Spaces:
Running
Running
Update chatbot_handler.py
Browse files- chatbot_handler.py +136 -82
chatbot_handler.py
CHANGED
@@ -1,112 +1,166 @@
|
|
1 |
# chatbot_handler.py
|
2 |
import logging
|
3 |
import json
|
4 |
-
import
|
5 |
-
|
6 |
|
7 |
-
#
|
8 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
9 |
|
10 |
def format_history_for_gemini(gradio_chat_history: list) -> list:
|
11 |
"""
|
12 |
Converts Gradio chat history (list of dicts with 'role' and 'content')
|
13 |
-
to Gemini API's 'contents' format.
|
14 |
-
Gemini expects roles 'user' and 'model'.
|
15 |
-
It also filters out system messages if any, as Gemini handles system prompts differently.
|
16 |
"""
|
17 |
gemini_contents = []
|
18 |
for msg in gradio_chat_history:
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
|
|
|
|
23 |
else:
|
24 |
-
logging.warning(f"Skipping non-string content in chat history for Gemini: {
|
25 |
return gemini_contents
|
26 |
|
27 |
-
async def generate_llm_response(user_message: str, plot_id: str, plot_label: str, chat_history_for_plot: list):
|
28 |
"""
|
29 |
-
Generates a response from the LLM using Gemini API.
|
30 |
Args:
|
31 |
user_message (str): The latest message from the user.
|
32 |
plot_id (str): The ID of the plot being discussed.
|
33 |
plot_label (str): The label of the plot being discussed.
|
34 |
chat_history_for_plot (list): The current conversation history for this plot.
|
35 |
-
This list
|
|
|
|
|
|
|
|
|
36 |
Returns:
|
37 |
str: The LLM's response text.
|
38 |
"""
|
39 |
logging.info(f"Generating LLM response for plot_id: {plot_id} ('{plot_label}'). User message: '{user_message}'")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
40 |
|
|
|
|
|
41 |
gemini_formatted_history = format_history_for_gemini(chat_history_for_plot)
|
42 |
|
43 |
if not gemini_formatted_history:
|
44 |
-
logging.error("Cannot generate LLM response: Formatted history is empty.")
|
45 |
return "I'm sorry, there was an issue processing the conversation history."
|
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 |
-
logging.error(f"Could not read raw text response: {read_err}")
|
109 |
-
return "Sorry, I received an unreadable response from the AI model."
|
110 |
-
except Exception as e:
|
111 |
-
logging.error(f"Generic error during LLM call for '{plot_label}': {e}", exc_info=True)
|
112 |
-
return f"An unexpected error occurred while trying to get an AI response: {type(e).__name__}."
|
|
|
1 |
# chatbot_handler.py
|
2 |
import logging
|
3 |
import json
|
4 |
+
import google.generativeai as genai
|
5 |
+
import os # For potential API key loading if Canvas injection fails for the library
|
6 |
|
7 |
+
# --- Gemini Configuration ---
|
8 |
+
# Option 1: Rely on Canvas to make this work with an empty key for the library.
|
9 |
+
# This is the preferred approach as per Canvas guidelines for 'fetch'.
|
10 |
+
GEMINI_API_KEY = ""
|
11 |
+
GEMINI_API_KEY = os.getenv('GEMINI_API_KEY')
|
12 |
+
|
13 |
+
# Option 2: Fallback to environment variable if direct empty key doesn't work with the library via Canvas.
|
14 |
+
# if not GEMINI_API_KEY: # This check would be if we explicitly want to load from env
|
15 |
+
# GEMINI_API_KEY = os.getenv('GEMINI_API_KEY_ENV_VAR_NAME') # Replace with your actual env var name if you use one
|
16 |
+
# if not GEMINI_API_KEY:
|
17 |
+
# logging.warning("GEMINI_API_KEY not found via direct assignment or environment variable.")
|
18 |
+
# If you have a default key for local testing (NOT FOR PRODUCTION/CANVAS)
|
19 |
+
# GEMINI_API_KEY = "YOUR_LOCAL_DEV_API_KEY"
|
20 |
+
|
21 |
+
model = None
|
22 |
+
gen_config = None
|
23 |
+
safety_settings = []
|
24 |
+
|
25 |
+
try:
|
26 |
+
if GEMINI_API_KEY is not None: # Check if it's set (even if empty string for Canvas)
|
27 |
+
genai.configure(api_key=GEMINI_API_KEY)
|
28 |
+
|
29 |
+
# As per general instructions, use gemini-2.0-flash if not told otherwise.
|
30 |
+
MODEL_NAME = "gemini-2.0-flash"
|
31 |
+
model = genai.GenerativeModel(MODEL_NAME)
|
32 |
+
|
33 |
+
gen_config = genai.types.GenerationConfig(
|
34 |
+
temperature=0.7,
|
35 |
+
top_k=1, # Per user's original config
|
36 |
+
top_p=1, # Per user's original config
|
37 |
+
max_output_tokens=2048, # Per user's original config
|
38 |
+
)
|
39 |
+
|
40 |
+
# Standard safety settings
|
41 |
+
safety_settings = [
|
42 |
+
{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
|
43 |
+
{"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
|
44 |
+
{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
|
45 |
+
{"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
|
46 |
+
]
|
47 |
+
logging.info(f"Gemini model '{MODEL_NAME}' configured successfully.")
|
48 |
+
else:
|
49 |
+
logging.error("Gemini API Key is None. Model not configured.")
|
50 |
+
|
51 |
+
except Exception as e:
|
52 |
+
logging.error(f"Failed to configure Gemini or instantiate model: {e}", exc_info=True)
|
53 |
+
model = None # Ensure model is None if setup fails
|
54 |
|
55 |
def format_history_for_gemini(gradio_chat_history: list) -> list:
|
56 |
"""
|
57 |
Converts Gradio chat history (list of dicts with 'role' and 'content')
|
58 |
+
to Gemini API's 'contents' format (list of dicts with 'role' and 'parts').
|
59 |
+
Gemini SDK expects roles 'user' and 'model'.
|
|
|
60 |
"""
|
61 |
gemini_contents = []
|
62 |
for msg in gradio_chat_history:
|
63 |
+
# Map Gradio 'assistant' role to Gemini 'model' role
|
64 |
+
role = "user" if msg["role"] == "user" else "model"
|
65 |
+
|
66 |
+
content = msg.get("content")
|
67 |
+
if isinstance(content, str):
|
68 |
+
gemini_contents.append({"role": role, "parts": [{"text": content}]})
|
69 |
else:
|
70 |
+
logging.warning(f"Skipping non-string content in chat history for Gemini: {content}")
|
71 |
return gemini_contents
|
72 |
|
73 |
+
async def generate_llm_response(user_message: str, plot_id: str, plot_label: str, chat_history_for_plot: list, plot_data_summary: str = None):
|
74 |
"""
|
75 |
+
Generates a response from the LLM using Gemini API via the Python SDK.
|
76 |
Args:
|
77 |
user_message (str): The latest message from the user.
|
78 |
plot_id (str): The ID of the plot being discussed.
|
79 |
plot_label (str): The label of the plot being discussed.
|
80 |
chat_history_for_plot (list): The current conversation history for this plot.
|
81 |
+
This list ALREADY includes the initial assistant message (with data summary)
|
82 |
+
and the latest user_message.
|
83 |
+
plot_data_summary (str, optional): The textual summary of the plot data.
|
84 |
+
While it's in the history, passing it explicitly might be useful
|
85 |
+
for future system prompt enhancements if needed.
|
86 |
Returns:
|
87 |
str: The LLM's response text.
|
88 |
"""
|
89 |
logging.info(f"Generating LLM response for plot_id: {plot_id} ('{plot_label}'). User message: '{user_message}'")
|
90 |
+
# Log the provided data summary for debugging
|
91 |
+
# logging.debug(f"Data summary for '{plot_label}':\n{plot_data_summary}")
|
92 |
+
|
93 |
+
|
94 |
+
if not model:
|
95 |
+
logging.error("Gemini model not configured. Cannot generate LLM response.")
|
96 |
+
return "I'm sorry, the AI model is not available at the moment. (Configuration Error)"
|
97 |
|
98 |
+
# The chat_history_for_plot already contains the initial assistant message with the summary,
|
99 |
+
# and the latest user message which triggered this call.
|
100 |
gemini_formatted_history = format_history_for_gemini(chat_history_for_plot)
|
101 |
|
102 |
if not gemini_formatted_history:
|
103 |
+
logging.error("Cannot generate LLM response: Formatted history is empty or invalid.")
|
104 |
return "I'm sorry, there was an issue processing the conversation history."
|
105 |
|
106 |
+
# Optional: Construct a system instruction if desired, though the initial message in history helps.
|
107 |
+
# system_instruction_text = (
|
108 |
+
# f"You are an expert in Employer Branding and LinkedIn social media strategy. "
|
109 |
+
# f"You are discussing the graph: '{plot_label}' (ID: '{plot_id}'). "
|
110 |
+
# f"A data summary for this graph was provided in your initial message: \n---\n{plot_data_summary}\n---\n"
|
111 |
+
# f"Refer to this summary and the conversation history to answer questions. "
|
112 |
+
# f"If specific data is not in the summary, clearly state that the provided snapshot doesn't contain that detail."
|
113 |
+
# )
|
114 |
+
# contents_for_api = [{"role": "system", "parts": [{"text": system_instruction_text}]}] + gemini_formatted_history
|
115 |
+
# For now, relying on the summary being in the `gemini_formatted_history` via the first assistant message.
|
116 |
+
|
117 |
+
try:
|
118 |
+
logging.debug(f"Sending to Gemini API. History: {json.dumps(gemini_formatted_history, indent=2)}")
|
119 |
+
|
120 |
+
response = await model.generate_content_async(
|
121 |
+
contents=gemini_formatted_history, # History already includes user's latest message
|
122 |
+
generation_config=gen_config,
|
123 |
+
safety_settings=safety_settings
|
124 |
+
)
|
125 |
+
|
126 |
+
# logging.debug(f"LLM API Raw Response object for '{plot_label}': {response}")
|
127 |
+
|
128 |
+
# Check for blocking based on prompt_feedback first (as per SDK examples)
|
129 |
+
if response.prompt_feedback and response.prompt_feedback.block_reason:
|
130 |
+
reason = response.prompt_feedback.block_reason.name # e.g., 'SAFETY'
|
131 |
+
# safety_ratings_info = [f"{rating.category.name}: {rating.probability.name}" for rating in response.prompt_feedback.safety_ratings]
|
132 |
+
# details = f" Safety Ratings: {', '.join(safety_ratings_info)}" if safety_ratings_info else ""
|
133 |
+
logging.warning(f"Content blocked by API (prompt_feedback) for '{plot_label}'. Reason: {reason}.")
|
134 |
+
return f"I'm sorry, your request was blocked due to content policy: {reason}."
|
135 |
+
|
136 |
+
# Accessing response text (handle multi-part if any)
|
137 |
+
if response.candidates and response.candidates[0].content and response.candidates[0].content.parts:
|
138 |
+
response_text = "".join(part.text for part in response.candidates[0].content.parts)
|
139 |
+
logging.info(f"LLM generated response for '{plot_label}': {response_text[:150]}...")
|
140 |
+
return response_text
|
141 |
+
else:
|
142 |
+
# This case might occur if the response was empty but not blocked by prompt_feedback
|
143 |
+
# (e.g. finish_reason other than SAFETY, or no candidates)
|
144 |
+
finish_reason_str = "UNKNOWN"
|
145 |
+
if response.candidates and response.candidates[0].finish_reason:
|
146 |
+
finish_reason_str = response.candidates[0].finish_reason.name # e.g. 'STOP', 'MAX_TOKENS', 'SAFETY', 'RECITATION', 'OTHER'
|
147 |
+
|
148 |
+
if finish_reason_str == 'SAFETY': # Content blocked at candidate level
|
149 |
+
logging.warning(f"Content blocked by API (candidate safety) for '{plot_label}'. Finish Reason: {finish_reason_str}.")
|
150 |
+
return f"I'm sorry, I can't provide a response due to safety filters regarding: {finish_reason_str}."
|
151 |
+
|
152 |
+
logging.error(f"Unexpected LLM API response structure or empty content for '{plot_label}'. Finish Reason: {finish_reason_str}. Full response: {response}")
|
153 |
+
return f"Sorry, I received an unexpected or empty response from the AI model (Finish Reason: {finish_reason_str})."
|
154 |
+
|
155 |
+
except google.api_core.exceptions.PermissionDenied as e:
|
156 |
+
logging.error(f"LLM API Permission Denied (Status 403) for '{plot_label}': {e}", exc_info=True)
|
157 |
+
return "Sorry, there's an issue with API permissions. Please ensure the API key is correct and the service is enabled. (Error 403)"
|
158 |
+
except google.api_core.exceptions.InvalidArgument as e:
|
159 |
+
logging.error(f"LLM API Invalid Argument (Status 400) for '{plot_label}': {e}. History: {json.dumps(gemini_formatted_history, indent=2)}", exc_info=True)
|
160 |
+
return "Sorry, there was an issue with the request sent to the AI model (e.g. malformed history). (Error 400)"
|
161 |
+
except google.api_core.exceptions.GoogleAPIError as e: # Catch other Google API errors
|
162 |
+
logging.error(f"Google API Error during LLM call for '{plot_label}': {e}", exc_info=True)
|
163 |
+
return f"An API error occurred while trying to get an AI response: {type(e).__name__}."
|
164 |
+
except Exception as e:
|
165 |
+
logging.error(f"Generic error during LLM call for '{plot_label}': {e}", exc_info=True)
|
166 |
+
return f"An unexpected error occurred while trying to get an AI response: {type(e).__name__}."
|
|
|
|
|
|
|
|
|
|