import os
import asyncio
import json
import sqlite3
import secrets # Import for generating secure session IDs
from dotenv import load_dotenv
# FIX: Added 'Response' to the import list
from flask import Flask, request, jsonify, g, redirect, url_for, make_response, Response
from google.adk.sessions import InMemorySessionService
from google.adk.runners import Runner
from google.genai.types import Content, Part
# NOTE: The 'instance.agent' module is assumed to be available in the execution environment.
# Ensure 'instance/agent.py' exists and exports a 'root_agent' instance for this to work.
try:
# If using ADK, this is where your agent definition lives
from instance.agent import root_agent
except ImportError:
print("WARNING: 'instance.agent' could not be imported. Agent functionality will be disabled.")
root_agent = None
# Load environment variables from .env file
load_dotenv()
# --- ADK Initialization & Global State ---
APP_NAME = "agent_flask"
USER_ID = "web_user" # Keeping a fixed user ID for this web demo
# --- Database Configuration ---
DATABASE = 'history.db'
# Initialize Flask App EARLY to ensure it's available for decorators
app = Flask(__name__)
# --- Database Functions ---
def get_db():
"""Returns a database connection, creating one if not present in flask.g."""
if 'db' not in g:
g.db = sqlite3.connect(DATABASE)
g.db.row_factory = sqlite3.Row # Allows accessing columns by name
return g.db
@app.teardown_appcontext
def close_db(e=None):
"""Closes the database connection at the end of the request."""
db = g.pop('db', None)
if db is not None:
db.close()
def init_db():
"""Initializes the database and creates the messages table."""
with app.app_context():
db = get_db()
# Create a table to store chat messages
db.execute("""
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
role TEXT NOT NULL, -- 'user' or 'agent'
text TEXT NOT NULL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
)
""")
db.commit()
def save_message(session_id: str, role: str, text: str):
"""Saves a single message to the database."""
try:
db = get_db()
db.execute(
"INSERT INTO messages (session_id, role, text) VALUES (?, ?, ?)",
(session_id, role, text)
)
db.commit()
except Exception as e:
app.logger.error(f"Database Save Error: {e}")
def load_history(session_id: str) -> list[dict]:
"""Loads all messages for a given session ID."""
try:
db = get_db()
rows = db.execute(
"SELECT role, text FROM messages WHERE session_id = ? ORDER BY timestamp ASC",
(session_id,)
).fetchall()
return [{"role": row['role'], "text": row['text']} for row in rows]
except Exception as e:
app.logger.error(f"Database Load Error: {e}")
return []
def get_all_session_ids() -> list[str]:
"""Loads all unique session IDs from the database."""
try:
db = get_db()
rows = db.execute(
# Order by timestamp of the last message in that session for better display logic
"""
SELECT T1.session_id FROM messages T1
INNER JOIN (
SELECT session_id, MAX(timestamp) AS max_timestamp
FROM messages
GROUP BY session_id
) T2 ON T1.session_id = T2.session_id AND T1.timestamp = T2.max_timestamp
GROUP BY T1.session_id
ORDER BY T2.max_timestamp DESC
"""
).fetchall()
return [row['session_id'] for row in rows]
except Exception as e:
app.logger.error(f"Database Session Load Error: {e}")
return []
# Initialize in-memory session service
session_service = InMemorySessionService()
# Create the runner with the agent only if root_agent was successfully imported
runner = None
adk_sessions = {} # Dictionary to track which ADK sessions have been created
if root_agent:
runner = Runner(
agent=root_agent,
app_name=APP_NAME,
session_service=session_service,
)
async def initialize_adk_session(session_id: str):
"""Creates the ADK session if it doesn't exist."""
if session_id not in adk_sessions:
app.logger.info(f"Creating ADK session for {USER_ID}/{session_id}")
await session_service.create_session(
app_name=APP_NAME,
user_id=USER_ID,
session_id=session_id
)
adk_sessions[session_id] = True
# --- Helper to get/create session ID from request ---
def get_or_create_session_id():
"""Gets the session ID from the request or generates a new one."""
session_id = request.args.get('session_id')
if not session_id:
# Generate a new, short, URL-safe session ID (e.g., 'a3b7c4d8')
session_id = secrets.token_hex(4)
# Redirect to the new URL with the session_id query parameter
return redirect(url_for('index', session_id=session_id))
return session_id
# --- API Endpoints ---
@app.route('/history', methods=['GET'])
def get_history_api():
"""Returns the chat history and all sessions for the current session ID."""
current_session_id = request.args.get('session_id')
if not current_session_id:
return jsonify({"history": [], "sessions": []}), 200
history = load_history(current_session_id)
sessions = get_all_session_ids()
return jsonify({
"history": history,
"current_session_id": current_session_id,
"sessions": sessions
})
@app.route('/chat', methods=['POST'])
def chat():
"""Handles incoming user messages, runs the ADK agent, and returns the response."""
current_session_id = request.args.get('session_id')
if not current_session_id:
return jsonify({"response": "Error: Session ID is missing."}), 400
if not runner:
return jsonify({"response": "Error: Agent runner is not initialized. Check server logs."}), 500
# Ensure the ADK session is initialized for this ID
if root_agent and current_session_id not in adk_sessions:
try:
# Synchronously call the async session creator
asyncio.run(initialize_adk_session(current_session_id))
except Exception as e:
app.logger.error(f"ADK Session Initialization Error: {e}")
return jsonify({"response": f"ADK Session Init Error: {str(e)}"}), 500
data = request.get_json()
user_input = data.get('message', '').strip()
if not user_input:
return jsonify({"response": "Please provide a message."}), 400
# 1. Save user message to history
save_message(current_session_id, "user", user_input)
# Prepare the message for the runner
message = Content(role="user", parts=[Part(text=user_input)])
response_text = "Sorry, I encountered an internal error."
async def get_agent_response(msg, session_id):
"""Asynchronously runs the agent and extracts the final text response."""
response = ""
try:
async for event in runner.run_async(
user_id=USER_ID,
session_id=session_id,
new_message=msg
):
if hasattr(event, "is_final_response") and event.is_final_response():
if hasattr(event, "content") and event.content.parts:
# Extract text from the first part of the content
response = event.content.parts[0].text
break
except Exception as e:
# Handle potential ADK/Runner exceptions
return f"An agent error occurred: {str(e)}"
return response
try:
final_response = asyncio.run(get_agent_response(message, current_session_id))
if final_response.startswith("An agent error occurred"):
response_text = final_response
status_code = 500
else:
response_text = final_response
status_code = 200
# 2. Save agent message to history on success
save_message(current_session_id, "agent", response_text)
except Exception as e:
response_text = f"Flask runtime error: {str(e)}"
status_code = 500
return jsonify({"response": response_text}), status_code
@app.route('/')
def index():
"""Handles dynamic session creation and serves the main chat application page."""
# 1. Check for/create session_id and handle redirect if needed
session_id_result = get_or_create_session_id()
# FIX: Check against the Flask Response class type, as flask.redirect() returns a Response object.
if isinstance(session_id_result, Response):
return session_id_result
current_session_id = session_id_result
# 2. Pass the current session ID to the HTML generator
html_content = get_html_content(current_session_id)
response = make_response(html_content)
return response
# --- Frontend HTML/JS/CSS (Inlined for single-file deployment) ---
def get_html_content(current_session_id):
"""Generates the single HTML page with inline CSS and JavaScript for the chat UI."""
html_template = f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ADK Agent Web Chat - Session {current_session_id}</title>
<!-- Load Tailwind CSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<style>
/* Custom Scrollbar for Chat Window */
.chat-window::-webkit-scrollbar {{
width: 8px;
}}
.chat-window::-webkit-scrollbar-thumb {{
background-color: #a3a3a3; /* gray-400 */
border-radius: 4px;
}}
.chat-window::-webkit-scrollbar-track {{
background-color: #f3f4f6; /* gray-100 */
}}
/* Use Inter font */
body {{
font-family: 'Inter', sans-serif;
}}
</style>
<script>
// Set up Tailwind configuration for the Inter font
tailwind.config = {{
theme: {{
extend: {{
fontFamily: {{
sans: ['Inter', 'sans-serif'],
}}
}}
}}
}}
</script>
</head>
<!-- MODIFIED: Changed body classes for full height layout -->
<body class="bg-gray-100 min-h-screen flex w-full">
<!-- Sidebar for Session History -->
<!-- MODIFIED: Switched from 'hidden md:block' to 'hidden lg:block'
and changed h-screen to h-full for better layout control -->
<div class="hidden lg:block w-64 bg-white shadow-xl ring-1 ring-gray-200 rounded-xl overflow-y-auto p-4 mr-4 flex-shrink-0 h-[calc(100vh-2rem)] mt-4 mb-4 ml-4">
<h2 class="text-xl font-bold text-gray-800 mb-4 border-b pb-2">Sessions</h2>
<a href="/" id="new-chat-link" class="block w-full text-center py-2 mb-4 bg-green-500 text-white font-semibold rounded-lg hover:bg-green-600 transition duration-200">
+ New Chat
</a>
<div id="session-list" class="space-y-1">
<!-- Session links will be populated here -->
</div>
</div>
<!-- Main Chat Area -->
<!-- MODIFIED: Used w-full and h-[calc(100vh-2rem)] for full height.
Removed redundant max-w-* classes that limit chat width on large screens. -->
<div class="w-full bg-white shadow-2xl ring-1 ring-gray-200 rounded-xl overflow-hidden flex flex-col h-[calc(100vh-2rem)] flex-grow mt-4 mb-4 mr-4">
<!-- Header -->
<header class="p-4 bg-indigo-600 text-white shadow-lg rounded-t-xl">
<h1 class="text-2xl font-extrabold tracking-tight">ADK Agent Chat</h1>
<p class="text-sm opacity-80 mt-1">Current Session: <b id="current-session-display">{current_session_id}</b></p>
</header>
<!-- Chat Window (Content will be filled by JavaScript on load) -->
<div id="chat-window" class="chat-window flex-grow overflow-y-auto p-4 space-y-4">
<!-- Chat messages go here -->
</div>
<!-- Input Form -->
<div class="p-4 border-t border-gray-200 bg-white">
<form id="chat-form" class="flex space-x-3">
<input
type="text"
id="user-input"
placeholder="Type your message here..."
required
autocomplete="off"
class="flex-grow p-3 border-2 border-gray-300 rounded-xl focus:ring-indigo-500 focus:border-indigo-500 transition duration-200"
/>
<button
type="submit"
id="send-button"
class="bg-indigo-600 text-white p-3 rounded-xl font-bold shadow-lg hover:bg-indigo-700 transition duration-200 active:scale-[0.98] disabled:bg-gray-400"
>
Send
</button>
</form>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {{
const currentSessionId = "{current_session_id}";
const form = document.getElementById('chat-form');
const userInput = document.getElementById('user-input');
const chatWindow = document.getElementById('chat-window');
const sendButton = document.getElementById('send-button');
const sessionList = document.getElementById('session-list');
// Function to add a message to the chat window
function addMessage(text, role) {{
const isUser = role === 'user';
const messageElement = document.createElement('div');
if (isUser) {{
messageElement.className = 'flex justify-end';
messageElement.innerHTML = `
<div class="bg-indigo-600 text-white p-4 rounded-xl rounded-br-sm max-w-[80%] shadow-lg break-words whitespace-pre-wrap">
<!-- Placeholder will be filled with text content -->
</div>
`;
}} else {{ // agent
messageElement.className = 'flex justify-start';
messageElement.innerHTML = `
<div class="bg-gray-200 text-gray-800 p-4 rounded-xl rounded-tl-sm max-w-[80%] shadow-lg break-words whitespace-pre-wrap">
<!-- Placeholder will be filled with text content -->
</div>
`;
}}
// Sanitize text and handle HTML content
const contentDiv = messageElement.querySelector('div:last-child');
// We allow basic HTML (like bold tags <b>) for welcome messages,
// but primarily set text content safely.
if (role === 'agent' && text.includes('<b')) {{
contentDiv.innerHTML = text;
}} else {{
contentDiv.textContent = text;
}}
chatWindow.appendChild(messageElement);
// Scroll to the latest message
chatWindow.scrollTop = chatWindow.scrollHeight;
}}
// Function to populate the sidebar with session links
function populateSessionList(sessions) {{
sessionList.innerHTML = ''; // Clear existing list
sessions.forEach(sessionId => {{
const link = document.createElement('a');
link.href = `/?session_id=${'{sessionId}'}`;
// **FIXED THE SYNTAX ERROR HERE:** Double-brace escaping the JavaScript template literal expression
link.className = `block p-2 text-sm rounded-lg hover:bg-gray-200 transition duration-150 truncate \
$${{sessionId}} === currentSessionId ? 'bg-indigo-100 font-semibold text-indigo-700' : 'text-gray-700'`;
link.textContent = `Chat #$${{sessionId}}`; // Escaped again for text content
sessionList.appendChild(link);
}});
}}
// Function to load and display chat history and sessions
async function loadChatData() {{
try {{
const response = await fetch(`/history?session_id=${'{currentSessionId}'}`);
const data = await response.json();
// 1. Clear chat window first
chatWindow.innerHTML = '';
// 2. Load History
const history = data.history || [];
if (history.length === 0) {{
addMessage(`Welcome to Chat #<b class='text-indigo-700'>${'{currentSessionId}'}</b>! I am your ADK Agent, ready to assist you. Ask me anything!`, 'agent');
}} else {{
history.forEach(msg => {{
if (msg.role && msg.text) {{
addMessage(msg.text, msg.role);
}}
}});
}}
// 3. Populate Session List
populateSessionList(data.sessions || []);
}} catch (error) {{
console.error('Failed to load chat data:', error);
// Fallback welcome message
chatWindow.innerHTML = '';
addMessage('Network or database error. Please refresh. If this is a new session, you can start chatting.', 'agent');
}}
}}
// Load chat data when the page loads
loadChatData();
// Function to show a loading state
function showLoading() {{
let loadingDiv = document.getElementById('loading-message');
if (!loadingDiv) {{
loadingDiv = document.createElement('div');
loadingDiv.id = 'loading-message';
loadingDiv.className = 'flex justify-start';
loadingDiv.innerHTML = `
<div class="bg-gray-200 text-gray-600 p-4 rounded-xl rounded-tl-sm max-w-[80%] shadow-md">
<span class="animate-pulse">Agent is thinking...</span>
</div>
`;
chatWindow.appendChild(loadingDiv);
chatWindow.scrollTop = chatWindow.scrollHeight;
}}
return loadingDiv;
}}
// Function to hide the loading state
function hideLoading() {{
const loadingDiv = document.getElementById('loading-message');
if (loadingDiv) {{
loadingDiv.remove();
}}
}}
// Handle form submission
form.addEventListener('submit', async (e) => {{
e.preventDefault();
const message = userInput.value.trim();
if (!message) return;
// 1. Display user message
addMessage(message, 'user');
// 2. Clear input and disable input/button
userInput.value = '';
sendButton.disabled = true;
userInput.disabled = true;
// 3. Show loading indicator
showLoading();
try {{
// 4. Send message to Flask backend, including session_id in the query
const response = await fetch(`/chat?session_id=${'{currentSessionId}'}`, {{
method: 'POST',
headers: {{
'Content-Type': 'application/json',
}},
body: JSON.stringify({{ message: message }})
}});
const data = await response.json();
// 5. Hide loading indicator
hideLoading();
// 6. Display agent response
if (response.ok) {{
addMessage(data.response, 'agent');
// After a successful chat, reload session list in case a new message updates the session list
loadChatData();
}} else {{
addMessage(`Error: ${'{data.response}'}`, 'agent');
console.error('Agent API Error:', data.response);
}}
}} catch (error) {{
// 5. Hide loading indicator on error
hideLoading();
// 6. Display network error
addMessage('Network Error: Could not reach the server.', 'agent');
console.error('Fetch Error:', error);
}} finally {{
// 7. Re-enable input/button
sendButton.disabled = false;
userInput.disabled = false;
userInput.focus();
}}
}});
}});
</script>
</body>
</html>
"""
return html_template
# --- Run the Flask App ---
if __name__ == "__main__":
# Initialize database when the application starts
init_db()
# To run this file, you'll need to:
# 1. Have 'instance/agent.py' (or mock it)
# 2. Install dependencies (flask, python-dotenv, google-genai, google-adk)
# 3. Run from the terminal: python app.py
app.run(debug=True, host='0.0.0.0', port=5000)