diff --git a/backend/chatbot_agent.py b/backend/chatbot_agent.py index 02656dd..d0eb37e 100644 --- a/backend/chatbot_agent.py +++ b/backend/chatbot_agent.py @@ -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 @@ -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) diff --git a/backend/main.py b/backend/main.py index 1f0f592..f05dea1 100644 --- a/backend/main.py +++ b/backend/main.py @@ -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, @@ -10,7 +9,7 @@ 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 @@ -18,7 +17,7 @@ from typing import Optional -from backend.chatbot_agent import create_chatbot_agent +from chatbot_agent import create_chatbot_agent # ─── Logging setup ─────────────────────────────────────────────────────── logging.basicConfig( @@ -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] diff --git a/frontends/streamlit-starter/app.py b/frontends/streamlit-starter/app.py index 6d65a88..f0204a9 100644 --- a/frontends/streamlit-starter/app.py +++ b/frontends/streamlit-starter/app.py @@ -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" @@ -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") @@ -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") @@ -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}")