Skip to content
Merged
28 changes: 13 additions & 15 deletions backend/chatbot_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,18 @@
)
logger = logging.getLogger("chatbot-server")

_prompt: str = ("You are an assistant. Your role is to answer user's question."
"Whenever an user ask a question, first search your documents, if you don't have enough information, do a web search."
"Once you have enough information, you can answer the user query.")


@tool
def search_documents(query: str, config: RunnableConfig) -> str:
"""
First search your own documents to see if you have enough information.
"""
logger.info(f"Searching scrapped web page for: {query}")
chunks = config["configurable"]["retriever"].invoke(query, k=8)
logger.info(chunks)
return "\n\n".join(chunk.page_content for chunk in chunks)
_prompt: str = (
"You are an expert AI assistant designed to help users by answering questions, providing explanations, and solving problems.\n"
"You have access to a web search tool.\n"
"Whenever a user asks a question, always consider if a web search could provide up-to-date or relevant information.\n"
"If so, use the web_search tool to gather facts, context, or recent data before answering.\n"
"Combine your own knowledge with the results of your web search to provide clear, accurate, and helpful answers.\n"
"If the user asks for a story, creative content, or advice, you may use your own reasoning and creativity, but always check if a web search could improve your response.\n"
"Be transparent about when you use web search.\n"
"If you cannot answer, or if the information is not available, say so honestly.\n"
"Always be concise, friendly, and professional.\n"
"If the user asks for sources, cite the web search results you used.\n"
)


@tool
Expand All @@ -56,4 +54,4 @@ def create_chatbot_agent(model_name: str) -> CompiledStateGraph:
temperature=0,
max_tokens=1024,
)
return create_react_agent(model=llm, prompt=_prompt, tools=[search_documents, web_search], state_schema=AgentStatePydantic)
return create_react_agent(model=llm, prompt=_prompt, tools=[web_search], state_schema=AgentStatePydantic)
94 changes: 32 additions & 62 deletions backend/main.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import logging

from langchain_community.document_loaders import WebBaseLoader
from langchain_community.retrievers import TFIDFRetriever

from langchain_core.messages import (
HumanMessage,
AIMessage, filter_messages,
Expand All @@ -10,15 +9,15 @@
from fastapi import FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from langchain_core.runnables import RunnableConfig
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langgraph.graph.state import CompiledStateGraph
from langgraph.prebuilt.chat_agent_executor import AgentStatePydantic
from pydantic import BaseModel
from mangum import Mangum

from typing import Optional

from backend.chatbot_agent import create_chatbot_agent
from chatbot_agent import create_chatbot_agent

# ─── Logging setup ───────────────────────────────────────────────────────
logging.basicConfig(
Expand Down Expand Up @@ -55,80 +54,51 @@ class UserMessage(BaseModel):
max_age=3600,
)

agent: Optional[CompiledStateGraph] = None
state: AgentStatePydantic = AgentStatePydantic(messages=[])
retriever: Optional[TFIDFRetriever] = None



def reset_chatbot(model_name):
global agent, state
agent = create_chatbot_agent(model_name)
state = AgentStatePydantic(messages=[AIMessage(
content=(
"Hello!\n\nI'm a personal assistant chatbot. "
"I will respond as best I can to any messages you send me."
)
)])


@app.post("/init")
async def init_index(
req: NewChatRequest,
):
global retriever

url = req.page_url.strip()
loader = WebBaseLoader(url)
docs = loader.load()
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
chunks = splitter.split_documents(docs)
retriever = TFIDFRetriever.from_documents(chunks)

return {
"status": "RAG index initialized",
"url": url,
"num_chunks": len(chunks),
"message": f"Successfully scraped and indexed {len(chunks),} chunks from {url}"
}


# ─── Chat endpoints ─────────────────────────────────────────────────────
@app.get("/")
async def get_chat_logs():
logger.info("Received GET /; returning chat_log")
return filter_messages(state.messages, exclude_tool_calls=True)
# No chat log stored on backend anymore
return []


@app.post("/")
async def chat(request: Request, user_input: UserMessage):
global agent, state

# 1) Log prompt
logger.info("Received chat POST; prompt=%s", user_input.prompt)

# 2) Get the model identifier from headers
async def chat(request: Request):
data = await request.json()
messages = data.get("messages", [])
logger.info(f"Injecting messages into agent state: {messages}")
model_id = request.headers.get("x-model-id")
logger.info(f"Received x-model-id header: {model_id}")
if not model_id:
raise HTTPException(status_code=400, detail="Missing x-model-id header")

# 4) Initialize ReAct agent if not already done
if agent is None:
logger.info("Initializing ReAct agent with model: %s", model_id)
reset_chatbot(model_id)

try:
state.messages += [HumanMessage(content=user_input.prompt)]
result = agent.invoke(input=state, config=RunnableConfig(configurable={'retriever': retriever}))
state = AgentStatePydantic.model_validate(result)

# Keep chat log manageable (last 20 messages)
if len(state.messages) > 20:
state.messages = state.messages[-20:]

except Exception as exc:
logger.error("ReAct agent error: %s", exc)
raise HTTPException(status_code=500, detail="Agent processing error")

# Create agent and state from scratch for each request
agent = create_chatbot_agent(model_id)
def build_state_messages(msgs):
state_messages = []
for m in msgs:
if m["type"] == "human":
state_messages.append(HumanMessage(content=m["content"]))
elif m["type"] == "ai":
state_messages.append(AIMessage(content=m["content"]))
elif m["type"] == "tool":
state_messages.append({"type": "tool", "name": m.get("name", "tool"), "content": m["content"]})
else:
state_messages.append(m)
return state_messages
state_messages = build_state_messages(messages)
state = AgentStatePydantic(messages=state_messages)
result = agent.invoke(input=state, config=RunnableConfig())
state = AgentStatePydantic.model_validate(result)
# Keep chat log manageable (last 20 messages)
if len(state.messages) > 20:
state.messages = state.messages[-20:]
return state.messages[-1]


Expand Down
77 changes: 25 additions & 52 deletions frontends/streamlit-starter/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
import requests
import time
import os
from dotenv import load_dotenv

load_dotenv()

# Backend API configuration
BACKEND_URL = "http://localhost:8000"
Expand All @@ -24,28 +27,7 @@
st.sidebar.title("💬 Chat Controls")
st.sidebar.markdown("---")

# RAG Initialization Section
st.sidebar.subheader("🔗 RAG Initialization")
rag_url = st.sidebar.text_input("Page URL", value="https://example.com", key="rag_url")

# Handle RAG initialization button click
if st.sidebar.button("Initialize RAG", key="init_rag_btn"):
try:
# Send POST request to initialize RAG with the provided URL
resp = requests.post(f"{BACKEND_URL}/init", json={"page_url": rag_url})
if resp.ok:
# Fetch updated chat history after successful RAG initialization
hist_resp = requests.get(f"{BACKEND_URL}/", headers={"x-model-id": st.session_state.get('model_select', '')})
if hist_resp.ok:
st.session_state["chat_history"] = hist_resp.json()
st.sidebar.success(f"RAG initialized! {resp.json()}")
st.rerun() # Force UI refresh to show updated state
else:
st.sidebar.error(f"Failed: {resp.text}")
except Exception as e:
st.sidebar.error(f"Error: {e}")

st.sidebar.markdown("---")
# RAG Initialization UI and logic removed (step 2)

# Model Selection Section
st.sidebar.subheader("🤖 Model Selection")
Expand All @@ -72,18 +54,9 @@ def fetch_model_ids():
# --- Main Chat Interface ---
st.title("IONOS Chatbot 🗨️")

# Initialize chat history from backend or create empty list
# Initialize chat history in frontend only
if "chat_history" not in st.session_state:
try:
# Fetch existing chat history from backend
resp = requests.get(f"{BACKEND_URL}/", headers={"x-model-id": model})
if resp.ok:
st.session_state["chat_history"] = resp.json()
else:
st.session_state["chat_history"] = []
except Exception:
# Fallback to empty history if backend is unavailable
st.session_state["chat_history"] = []
st.session_state["chat_history"] = []

# Display chat messages in bubble format
st.markdown("#### Conversation")
Expand Down Expand Up @@ -116,32 +89,32 @@ def fetch_model_ids():
if send_btn and user_message.strip():
# Add user message to chat history immediately for better UX
st.session_state["chat_history"].append({"type": "human", "content": user_message})

# Send message to backend and get AI response
# Ensure we never send an empty messages array
messages_to_send = st.session_state["chat_history"] if st.session_state["chat_history"] else [{"type": "human", "content": user_message}]
with st.spinner("Bot is thinking..."):
try:
# Post user message to backend with selected model
resp = requests.post(
f"{BACKEND_URL}/",
json={"prompt": user_message},
json={"messages": messages_to_send},
headers={"x-model-id": model},
)
if resp.ok:
# Add AI response to chat history
st.session_state["chat_history"].append({"type": "ai", "content": resp.text})

# Attempt to sync with backend chat history
try:
hist_resp = requests.get(f"{BACKEND_URL}/", headers={"x-model-id": model})
if hist_resp.ok:
backend_history = hist_resp.json()
# Update local history if backend has more recent messages
if len(backend_history) > len(st.session_state["chat_history"]):
st.session_state["chat_history"] = backend_history
except Exception:
# Continue with local history if backend sync fails
pass

# Parse backend response (backend always returns a single message dict)
data = resp.json() if resp.headers.get('content-type','').startswith('application/json') else {"type": "ai", "content": resp.text}
if not isinstance(data, dict):
st.error("Unexpected response shape from backend (expected object)")
else:
if data.get("type") == "tool":
st.session_state["chat_history"].append({
"type": "tool",
"name": data.get("name", "tool"),
"content": data.get("content", "")
})
else:
st.session_state["chat_history"].append({
"type": data.get("type", "ai"),
"content": data.get("content", "")
})
st.rerun() # Refresh UI to show new messages
else:
st.error(f"Failed: {resp.text}")
Expand Down