diff --git a/Makefile b/Makefile index 2e5c9033..bfd810c5 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,27 @@ -.PHONY: help dev-frontend dev-backend dev +.PHONY: help install install-backend install-frontend dev-frontend dev-backend dev help: @echo "Available commands:" - @echo " make dev-frontend - Starts the frontend development server (Vite)" - @echo " make dev-backend - Starts the backend development server (Uvicorn with reload)" - @echo " make dev - Starts both frontend and backend development servers" + @echo " make install - Installs all dependencies for Windows" + @echo " make dev - Starts both frontend and backend servers for Windows" + @echo " ---" + @echo " make install-backend - Installs backend dependencies" + @echo " make install-frontend - Installs frontend dependencies" + @echo " make dev-frontend - Starts the frontend development server (Vite)" + @echo " make dev-backend - Starts the backend development server (Langgraph)" + + +install: install-backend install-frontend + +install-backend: + @echo "Checking for backend virtual environment..." + @if not exist backend\venv (py -m venv backend\venv) + @echo "Installing backend dependencies..." + @call backend\venv\Scripts\activate.bat && pip install -e backend + +install-frontend: + @echo "Installing frontend dependencies..." + @cd frontend && npm install dev-frontend: @echo "Starting frontend development server..." @@ -12,9 +29,11 @@ dev-frontend: dev-backend: @echo "Starting backend development server..." - @cd backend && langgraph dev + @echo "Activating backend environment and starting server..." + @cd backend && call .\venv\Scripts\activate.bat && langgraph dev # Run frontend and backend concurrently dev: - @echo "Starting both frontend and backend development servers..." - @make dev-frontend & make dev-backend \ No newline at end of file + @echo "Starting both frontend and backend development servers in new windows..." + @start "Frontend" make dev-frontend + @start "Backend" make dev-backend \ No newline at end of file diff --git a/backend/src/agent/app.py b/backend/src/agent/app.py index f20f6ed3..8108794d 100644 --- a/backend/src/agent/app.py +++ b/backend/src/agent/app.py @@ -1,11 +1,31 @@ # mypy: disable - error - code = "no-untyped-def,misc" import pathlib -from fastapi import FastAPI, Response +from typing import List, Any +from fastapi import FastAPI, Response, Body from fastapi.staticfiles import StaticFiles +from pydantic import BaseModel # Define the FastAPI app app = FastAPI() +# In-memory store for sessions +sessions_db: List[Any] = [] + + +class SessionItem(BaseModel): + session_id: str + messages: List[Any] # Replace Any with your Message model if available + historicalActivities: Any # Replace Any with your Activity model if available + +@app.post("/sessions") +async def save_session(session_item: SessionItem = Body(...)): + sessions_db.append(session_item.dict()) + return {"status": "ok", "session_id": session_item.session_id} + +@app.get("/sessions") +async def get_sessions() -> List[Any]: + return sessions_db + def create_frontend_router(build_dir="../frontend/dist"): """Creates a router to serve the React frontend. @@ -18,6 +38,32 @@ def create_frontend_router(build_dir="../frontend/dist"): """ build_path = pathlib.Path(__file__).parent.parent.parent / build_dir + print(f"DEBUG: __file__ is {__file__}") + print(f"DEBUG: build_dir is {build_dir}") + print(f"DEBUG: Calculated build_path is {build_path}") + try: + abs_build_path = build_path.resolve() + print(f"DEBUG: Absolute build_path is {abs_build_path}") + print(f"DEBUG: build_path exists: {abs_build_path.exists()}") + print(f"DEBUG: build_path is_dir: {abs_build_path.is_dir()}") + index_html_path = abs_build_path / "index.html" + print(f"DEBUG: index_html_path is {index_html_path}") + print(f"DEBUG: index_html_path exists: {index_html_path.exists()}") + print(f"DEBUG: index_html_path is_file: {index_html_path.is_file()}") + + # Try to list directories - this might fail if paths are incorrect or due to permissions + try: + frontend_dir_to_list = pathlib.Path("C:/AI-Programmi/new/gemini-fullstack-langgraph-quickstart/frontend") + print(f"DEBUG: Listing {frontend_dir_to_list}: {list(frontend_dir_to_list.iterdir()) if frontend_dir_to_list.exists() else 'DOES NOT EXIST'}") + frontend_dist_dir_to_list = pathlib.Path("C:/AI-Programmi/new/gemini-fullstack-langgraph-quickstart/frontend/dist") + print(f"DEBUG: Listing {frontend_dist_dir_to_list}: {list(frontend_dist_dir_to_list.iterdir()) if frontend_dist_dir_to_list.exists() else 'DOES NOT EXIST'}") + except Exception as e: + print(f"DEBUG: Error listing directories: {e}") + + except Exception as e: + print(f"DEBUG: Error resolving or checking build_path: {e}") + + if not build_path.is_dir() or not (build_path / "index.html").is_file(): print( f"WARN: Frontend build directory not found or incomplete at {build_path}. Serving frontend will likely fail." diff --git a/backup/App.tsx.bak b/backup/App.tsx.bak new file mode 100644 index 00000000..d2933008 --- /dev/null +++ b/backup/App.tsx.bak @@ -0,0 +1,232 @@ +import { useStream } from "@langchain/langgraph-sdk/react"; +import type { Message } from "@langchain/langgraph-sdk"; +import { useState, useEffect, useRef, useCallback } from "react"; +import { ProcessedEvent } from "@/components/ActivityTimeline"; +import { WelcomeScreen } from "@/components/WelcomeScreen"; +import { ChatMessagesView } from "@/components/ChatMessagesView"; +import { HistoryPanel } from "@/components/HistoryPanel"; + +const API_URL = import.meta.env.DEV + ? "http://localhost:2024" + : "http://localhost:8123"; + +export default function App() { + const [processedEventsTimeline, setProcessedEventsTimeline] = useState< + ProcessedEvent[] + >([]); + const [historicalActivities, setHistoricalActivities] = useState< + Record + >({}); + const scrollAreaRef = useRef(null); + const hasFinalizeEventOccurredRef = useRef(false); + const [history, setHistory] = useState([]); + const [threadId, setThreadId] = useState(null); + const [messages, setMessages] = useState([]); + const thread = useStream<{ + messages: Message[]; + initial_search_query_count: number; + max_research_loops: number; + reasoning_model: string; + }>({ + apiUrl: API_URL, + assistantId: "agent", + threadId, + onThreadId: setThreadId, + messagesKey: "messages", + onUpdateEvent: (event: any) => { + let processedEvent: ProcessedEvent | null = null; + if (event.generate_query) { + processedEvent = { + title: "Generating Search Queries", + data: event.generate_query?.search_query?.join(", ") || "", + }; + } else if (event.web_research) { + const sources = event.web_research.sources_gathered || []; + const numSources = sources.length; + const uniqueLabels = [ + ...new Set(sources.map((s: any) => s.label).filter(Boolean)), + ]; + const exampleLabels = uniqueLabels.slice(0, 3).join(", "); + processedEvent = { + title: "Web Research", + data: `Gathered ${numSources} sources. Related to: ${ + exampleLabels || "N/A" + }.`, + }; + } else if (event.reflection) { + processedEvent = { + title: "Reflection", + data: "Analysing Web Research Results", + }; + } else if (event.finalize_answer) { + processedEvent = { + title: "Finalizing Answer", + data: "Composing and presenting the final answer.", + }; + hasFinalizeEventOccurredRef.current = true; + } + if (processedEvent) { + setProcessedEventsTimeline((prevEvents) => [ + ...prevEvents, + processedEvent!, + ]); + } + }, + onFinish: async (threadState) => { + if (threadId) { + const lastMessage = + threadState.values.messages[ + threadState.values.messages.length - 1 + ]; + let updatedHistoricalActivities = { ...historicalActivities }; + if (lastMessage && lastMessage.type === "ai" && lastMessage.id) { + updatedHistoricalActivities = { + ...historicalActivities, + [lastMessage.id]: [...processedEventsTimeline], + }; + setHistoricalActivities(updatedHistoricalActivities); + } + + const newHistoryItem = { + session_id: threadId, + messages: threadState.values.messages, + historicalActivities: updatedHistoricalActivities, + }; + + try { + await fetch(`${API_URL}/sessions`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(newHistoryItem), + }); + setHistory((prevHistory) => [newHistoryItem, ...prevHistory]); + } catch (error) { + console.error("Failed to save history:", error); + } + } + }, + onError: (error: any) => { + console.error(error); + }, + }); + + useEffect(() => { + async function fetchHistory() { + try { + const response = await fetch(`${API_URL}/sessions`); + const data = await response.json(); + setHistory(data); + } catch (error) { + console.error("Failed to fetch history:", error); + } + } + fetchHistory(); + }, []); + + useEffect(() => { + if (scrollAreaRef.current) { + const scrollViewport = scrollAreaRef.current.querySelector( + "[data-radix-scroll-area-viewport]" + ); + if (scrollViewport) { + scrollViewport.scrollTop = scrollViewport.scrollHeight; + } + } + }, [messages]); + + useEffect(() => { + if (thread.messages.length > messages.length) { + setMessages(thread.messages); + } + }, [thread.messages, messages]); + + const handleSelectHistory = (historyItem: any) => { + setThreadId(historyItem.session_id); + setMessages(historyItem.messages); + setHistoricalActivities(historyItem.historicalActivities || {}); + setProcessedEventsTimeline([]); + }; + + const handleSubmit = useCallback( + async (submittedInputValue: string, effort: string, model: string) => { + if (!submittedInputValue.trim() || thread.isLoading) return; + setProcessedEventsTimeline([]); + hasFinalizeEventOccurredRef.current = false; + if (!threadId) { + setThreadId(new Date().toISOString()); + } + + // convert effort to, initial_search_query_count and max_research_loops + // low means max 1 loop and 1 query + // medium means max 3 loops and 3 queries + // high means max 10 loops and 5 queries + let initial_search_query_count = 0; + let max_research_loops = 0; + switch (effort) { + case "low": + initial_search_query_count = 1; + max_research_loops = 1; + break; + case "medium": + initial_search_query_count = 3; + max_research_loops = 3; + break; + case "high": + initial_search_query_count = 5; + max_research_loops = 10; + break; + } + + const newMessages: Message[] = [ + ...messages, + { + type: "human", + content: submittedInputValue, + id: Date.now().toString(), + }, + ]; + setMessages(newMessages); + thread.submit({ + messages: newMessages, + initial_search_query_count: initial_search_query_count, + max_research_loops: max_research_loops, + reasoning_model: model, + }); + }, + [thread, threadId, messages] + ); + + const handleCancel = useCallback(() => { + thread.stop(); + }, [thread]); + + return ( +
+ +
+ {messages.length === 0 ? ( + + ) : ( + + )} +
+
+ ); +} \ No newline at end of file diff --git a/backup/App.tsx.bak2 b/backup/App.tsx.bak2 new file mode 100644 index 00000000..c6ee2a6c --- /dev/null +++ b/backup/App.tsx.bak2 @@ -0,0 +1,236 @@ +import { useStream } from "@langchain/langgraph-sdk/react"; +import type { Message } from "@langchain/langgraph-sdk"; +import { useState, useEffect, useRef, useCallback } from "react"; +import { ProcessedEvent } from "@/components/ActivityTimeline"; +import { WelcomeScreen } from "@/components/WelcomeScreen"; +import { ChatMessagesView } from "@/components/ChatMessagesView"; +import { HistoryPanel } from "@/components/HistoryPanel"; + +const API_URL = import.meta.env.DEV + ? "http://localhost:2024" + : "http://localhost:8123"; + +export default function App() { + const [processedEventsTimeline, setProcessedEventsTimeline] = useState< + ProcessedEvent[] + >([]); + const [historicalActivities, setHistoricalActivities] = useState< + Record + >({}); + const scrollAreaRef = useRef(null); + const hasFinalizeEventOccurredRef = useRef(false); + const [history, setHistory] = useState([]); + const [threadId, setThreadId] = useState(null); + const [messages, setMessages] = useState([]); + const [streamingCompleted, setStreamingCompleted] = useState(false); + + const thread = useStream<{ + messages: Message[]; + initial_search_query_count: number; + max_research_loops: number; + reasoning_model: string; + }>({ + apiUrl: API_URL, + assistantId: "agent", + threadId, + onThreadId: setThreadId, + messagesKey: "messages", + onUpdateEvent: (event: any) => { + let processedEvent: ProcessedEvent | null = null; + if (event.generate_query) { + processedEvent = { + title: "Generating Search Queries", + data: event.generate_query?.search_query?.join(", ") || "", + }; + } else if (event.web_research) { + const sources = event.web_research.sources_gathered || []; + const numSources = sources.length; + const uniqueLabels = [ + ...new Set(sources.map((s: any) => s.label).filter(Boolean)), + ]; + const exampleLabels = uniqueLabels.slice(0, 3).join(", "); + processedEvent = { + title: "Web Research", + data: `Gathered ${numSources} sources. Related to: ${ + exampleLabels || "N/A" + }.`, + }; + } else if (event.reflection) { + processedEvent = { + title: "Reflection", + data: "Analysing Web Research Results", + }; + } else if (event.finalize_answer) { + processedEvent = { + title: "Finalizing Answer", + data: "Composing and presenting the final answer.", + }; + hasFinalizeEventOccurredRef.current = true; + } + if (processedEvent) { + setProcessedEventsTimeline((prevEvents) => [ + ...prevEvents, + processedEvent!, + ]); + } + }, + onFinish: async (threadState) => { + if (threadId) { + const lastMessage = + threadState.values.messages[ + threadState.values.messages.length - 1 + ]; + let updatedHistoricalActivities = { ...historicalActivities }; + if (lastMessage && lastMessage.type === "ai" && lastMessage.id) { + updatedHistoricalActivities = { + ...historicalActivities, + [lastMessage.id]: [...processedEventsTimeline], + }; + setHistoricalActivities(updatedHistoricalActivities); + } + + const newHistoryItem = { + session_id: threadId, + messages: threadState.values.messages, + historicalActivities: updatedHistoricalActivities, + }; + + try { + await fetch(`${API_URL}/sessions`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(newHistoryItem), + }); + setHistory((prevHistory) => [newHistoryItem, ...prevHistory]); + setStreamingCompleted(true); + } catch (error) { + console.error("Failed to save history:", error); + } + } + }, + onError: (error: any) => { + console.error(error); + }, + }); + + useEffect(() => { + async function fetchHistory() { + try { + const response = await fetch(`${API_URL}/sessions`); + const data = await response.json(); + setHistory(data); + } catch (error) { + console.error("Failed to fetch history:", error); + } + } + fetchHistory(); + }, []); + + useEffect(() => { + if (scrollAreaRef.current) { + const scrollViewport = scrollAreaRef.current.querySelector( + "[data-radix-scroll-area-viewport]" + ); + if (scrollViewport) { + scrollViewport.scrollTop = scrollViewport.scrollHeight; + } + } + }, [messages]); + + useEffect(() => { + if (streamingCompleted) { + setMessages(thread.messages); + setStreamingCompleted(false); + } + }, [streamingCompleted, thread.messages]); + + const handleSelectHistory = (historyItem: any) => { + setThreadId(historyItem.session_id); + setMessages(historyItem.messages); + setHistoricalActivities(historyItem.historicalActivities || {}); + setProcessedEventsTimeline([]); + }; + + const handleSubmit = useCallback( + async (submittedInputValue: string, effort: string, model: string) => { + if (!submittedInputValue.trim() || thread.isLoading) return; + setProcessedEventsTimeline([]); + hasFinalizeEventOccurredRef.current = false; + if (!threadId) { + setThreadId(new Date().toISOString()); + } + + // convert effort to, initial_search_query_count and max_research_loops + // low means max 1 loop and 1 query + // medium means max 3 loops and 3 queries + // high means max 10 loops and 5 queries + let initial_search_query_count = 0; + let max_research_loops = 0; + switch (effort) { + case "low": + initial_search_query_count = 1; + max_research_loops = 1; + break; + case "medium": + initial_search_query_count = 3; + max_research_loops = 3; + break; + case "high": + initial_search_query_count = 5; + max_research_loops = 10; + break; + } + + const newMessages: Message[] = [ + ...messages, + { + type: "human", + content: submittedInputValue, + id: Date.now().toString(), + }, + ]; + setMessages(newMessages); + thread.submit({ + messages: newMessages, + initial_search_query_count: initial_search_query_count, + max_research_loops: max_research_loops, + reasoning_model: model, + }); + }, + [thread, threadId, messages] + ); + + const handleCancel = useCallback(() => { + thread.stop(); + }, [thread]); + + return ( +
+ +
+ {messages.length === 0 ? ( + + ) : ( + + )} +
+
+ ); +} \ No newline at end of file diff --git a/backup/ChatMessagesView.tsx.bak b/backup/ChatMessagesView.tsx.bak new file mode 100644 index 00000000..1a245d88 --- /dev/null +++ b/backup/ChatMessagesView.tsx.bak @@ -0,0 +1,322 @@ +import type React from "react"; +import type { Message } from "@langchain/langgraph-sdk"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Loader2, Copy, CopyCheck } from "lucide-react"; +import { InputForm } from "@/components/InputForm"; +import { Button } from "@/components/ui/button"; +import { useState, ReactNode } from "react"; +import ReactMarkdown from "react-markdown"; +import { cn } from "@/lib/utils"; +import { Badge } from "@/components/ui/badge"; +import { + ActivityTimeline, + ProcessedEvent, +} from "@/components/ActivityTimeline"; // Assuming ActivityTimeline is in the same dir or adjust path + +// Markdown component props type from former ReportView +type MdComponentProps = { + className?: string; + children?: ReactNode; + [key: string]: any; +}; + +// Markdown components (from former ReportView.tsx) +const mdComponents = { + h1: ({ className, children, ...props }: MdComponentProps) => ( +

+ {children} +

+ ), + h2: ({ className, children, ...props }: MdComponentProps) => ( +

+ {children} +

+ ), + h3: ({ className, children, ...props }: MdComponentProps) => ( +

+ {children} +

+ ), + p: ({ className, children, ...props }: MdComponentProps) => ( +

+ {children} +

+ ), + a: ({ className, children, href, ...props }: MdComponentProps) => ( + + + {children} + + + ), + ul: ({ className, children, ...props }: MdComponentProps) => ( +
    + {children} +
+ ), + ol: ({ className, children, ...props }: MdComponentProps) => ( +
    + {children} +
+ ), + li: ({ className, children, ...props }: MdComponentProps) => ( +
  • + {children} +
  • + ), + blockquote: ({ className, children, ...props }: MdComponentProps) => ( +
    + {children} +
    + ), + code: ({ className, children, ...props }: MdComponentProps) => ( + + {children} + + ), + pre: ({ className, children, ...props }: MdComponentProps) => ( +
    +      {children}
    +    
    + ), + hr: ({ className, ...props }: MdComponentProps) => ( +
    + ), + table: ({ className, children, ...props }: MdComponentProps) => ( +
    + + {children} +
    +
    + ), + th: ({ className, children, ...props }: MdComponentProps) => ( + + {children} + + ), + td: ({ className, children, ...props }: MdComponentProps) => ( + + {children} + + ), +}; + +// Props for HumanMessageBubble +interface HumanMessageBubbleProps { + message: Message; + mdComponents: typeof mdComponents; +} + +// HumanMessageBubble Component +const HumanMessageBubble: React.FC = ({ + message, + mdComponents, +}) => { + return ( +
    + + {typeof message.content === "string" + ? message.content + : JSON.stringify(message.content)} + +
    + ); +}; + +// Props for AiMessageBubble +interface AiMessageBubbleProps { + message: Message; + historicalActivity: ProcessedEvent[] | undefined; + liveActivity: ProcessedEvent[] | undefined; + isLastMessage: boolean; + isOverallLoading: boolean; + mdComponents: typeof mdComponents; + handleCopy: (text: string, messageId: string) => void; + copiedMessageId: string | null; +} + +// AiMessageBubble Component +const AiMessageBubble: React.FC = ({ + message, + historicalActivity, + liveActivity, + isLastMessage, + isOverallLoading, + mdComponents, + handleCopy, + copiedMessageId, +}) => { + // Determine which activity events to show and if it's for a live loading message + const activityForThisBubble = + isLastMessage && isOverallLoading ? liveActivity : historicalActivity; + const isLiveActivityForThisBubble = isLastMessage && isOverallLoading; + + return ( +
    + {activityForThisBubble && activityForThisBubble.length > 0 && ( +
    + +
    + )} + + {typeof message.content === "string" + ? message.content + : JSON.stringify(message.content)} + + +
    + ); +}; + +interface ChatMessagesViewProps { + messages: Message[]; + isLoading: boolean; + scrollAreaRef: React.RefObject; + onSubmit: (inputValue: string, effort: string, model: string) => void; + onCancel: () => void; + liveActivityEvents: ProcessedEvent[]; + historicalActivities: Record; +} + +export function ChatMessagesView({ + messages, + isLoading, + scrollAreaRef, + onSubmit, + onCancel, + liveActivityEvents, + historicalActivities, +}: ChatMessagesViewProps) { + const [copiedMessageId, setCopiedMessageId] = useState(null); + + const handleCopy = async (text: string, messageId: string) => { + try { + await navigator.clipboard.writeText(text); + setCopiedMessageId(messageId); + setTimeout(() => setCopiedMessageId(null), 2000); // Reset after 2 seconds + } catch (err) { + console.error("Failed to copy text: ", err); + } + }; + return ( +
    + +
    + {messages.map((message, index) => { + const isLast = index === messages.length - 1; + return ( +
    +
    + {message.type === "human" ? ( + + ) : ( + + )} +
    +
    + ); + })} + {isLoading && + (messages.length === 0 || + messages[messages.length - 1].type === "human") && ( +
    + {" "} + {/* AI message row structure */} +
    + {liveActivityEvents.length > 0 ? ( +
    + +
    + ) : ( +
    + + Processing... +
    + )} +
    +
    + )} +
    +
    + 0} + /> +
    + ); +} diff --git a/backup/HistoryPanel.tsx.bak b/backup/HistoryPanel.tsx.bak new file mode 100644 index 00000000..0adf7347 --- /dev/null +++ b/backup/HistoryPanel.tsx.bak @@ -0,0 +1,36 @@ +import React from 'react'; + +interface HistoryPanelProps { + history: any[]; + onSelectHistory: (historyItem: any) => void; +} + +export const HistoryPanel: React.FC = ({ + history, + onSelectHistory, +}) => { + return ( +
    +

    History

    +
    + {history.map((historyItem, index) => ( +
    onSelectHistory(historyItem)} + > +

    + {historyItem.messages[0]?.content} +

    +

    + {new Date(historyItem.session_id).toLocaleString()} +

    +
    + ))} +
    +
    + ); +}; diff --git a/backup/WelcomeScreen.tsx.bak b/backup/WelcomeScreen.tsx.bak new file mode 100644 index 00000000..b1015aa8 --- /dev/null +++ b/backup/WelcomeScreen.tsx.bak @@ -0,0 +1,39 @@ +import { InputForm } from "./InputForm"; + +interface WelcomeScreenProps { + handleSubmit: ( + submittedInputValue: string, + effort: string, + model: string + ) => void; + onCancel: () => void; + isLoading: boolean; +} + +export const WelcomeScreen: React.FC = ({ + handleSubmit, + onCancel, + isLoading, +}) => ( +
    +
    +

    + Welcome. +

    +

    + How can I help you today? +

    +
    +
    + +
    +

    + Powered by Google Gemini and LangChain LangGraph. +

    +
    +); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d06d4021..28cdecad 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4,7 +4,11 @@ import { useState, useEffect, useRef, useCallback } from "react"; import { ProcessedEvent } from "@/components/ActivityTimeline"; import { WelcomeScreen } from "@/components/WelcomeScreen"; import { ChatMessagesView } from "@/components/ChatMessagesView"; -import { Button } from "@/components/ui/button"; +import { HistoryPanel } from "@/components/HistoryPanel"; + +const API_URL = import.meta.env.DEV + ? "http://localhost:2024" + : "http://localhost:8123"; export default function App() { const [processedEventsTimeline, setProcessedEventsTimeline] = useState< @@ -15,17 +19,21 @@ export default function App() { >({}); const scrollAreaRef = useRef(null); const hasFinalizeEventOccurredRef = useRef(false); - const [error, setError] = useState(null); + const [history, setHistory] = useState([]); + const [threadId, setThreadId] = useState(null); + const [messages, setMessages] = useState([]); + const [streamingCompleted, setStreamingCompleted] = useState(false); + const thread = useStream<{ messages: Message[]; initial_search_query_count: number; max_research_loops: number; reasoning_model: string; }>({ - apiUrl: import.meta.env.DEV - ? "http://localhost:2024" - : "http://localhost:8123", + apiUrl: API_URL, assistantId: "agent", + threadId, + onThreadId: setThreadId, messagesKey: "messages", onUpdateEvent: (event: any) => { let processedEvent: ProcessedEvent | null = null; @@ -66,11 +74,71 @@ export default function App() { ]); } }, + onFinish: async (threadState) => { + if (threadId) { + const lastMessage = + threadState.values.messages[ + threadState.values.messages.length - 1 + ]; + let updatedHistoricalActivities = { ...historicalActivities }; + if (lastMessage && lastMessage.type === "ai" && lastMessage.id) { + updatedHistoricalActivities = { + ...historicalActivities, + [lastMessage.id]: [...processedEventsTimeline], + }; + setHistoricalActivities(updatedHistoricalActivities); + } + + const newHistoryItem = { + session_id: threadId, + messages: threadState.values.messages, + historicalActivities: updatedHistoricalActivities, + }; + + try { + await fetch(`${API_URL}/sessions`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(newHistoryItem), + }); + setHistory((prevHistory) => [newHistoryItem, ...prevHistory]); + setStreamingCompleted(true); + } catch (error) { + console.error("Failed to save history:", error); + } + } + }, onError: (error: any) => { - setError(error.message); + console.error(error); }, }); + useEffect(() => { + async function fetchHistory() { + try { + const response = await fetch(`${API_URL}/sessions`); + if (!response.ok) { + console.error("Failed to fetch history: Network response was not ok", response.statusText); + setHistory([]); // Ensure history is an array even on error + return; + } + const data = await response.json(); + if (Array.isArray(data)) { + setHistory(data); + } else { + console.error("Failed to fetch history: Data is not an array", data); + setHistory([]); // Ensure history is an array if data format is incorrect + } + } catch (error) { + console.error("Failed to fetch history:", error); + setHistory([]); // Ensure history is an array on any other error + } + } + fetchHistory(); + }, []); + useEffect(() => { if (scrollAreaRef.current) { const scrollViewport = scrollAreaRef.current.querySelector( @@ -80,30 +148,30 @@ export default function App() { scrollViewport.scrollTop = scrollViewport.scrollHeight; } } - }, [thread.messages]); + }, [messages]); useEffect(() => { - if ( - hasFinalizeEventOccurredRef.current && - !thread.isLoading && - thread.messages.length > 0 - ) { - const lastMessage = thread.messages[thread.messages.length - 1]; - if (lastMessage && lastMessage.type === "ai" && lastMessage.id) { - setHistoricalActivities((prev) => ({ - ...prev, - [lastMessage.id!]: [...processedEventsTimeline], - })); - } - hasFinalizeEventOccurredRef.current = false; + if (streamingCompleted) { + setMessages(thread.messages); + setStreamingCompleted(false); } - }, [thread.messages, thread.isLoading, processedEventsTimeline]); + }, [streamingCompleted, thread.messages]); + + const handleSelectHistory = (historyItem: any) => { + setThreadId(historyItem.session_id); + setMessages(historyItem.messages); + setHistoricalActivities(historyItem.historicalActivities || {}); + setProcessedEventsTimeline([]); + }; const handleSubmit = useCallback( - (submittedInputValue: string, effort: string, model: string) => { - if (!submittedInputValue.trim()) return; + async (submittedInputValue: string, effort: string, model: string) => { + if (!submittedInputValue.trim() || thread.isLoading) return; setProcessedEventsTimeline([]); hasFinalizeEventOccurredRef.current = false; + if (!threadId) { + setThreadId(new Date().toISOString()); + } // convert effort to, initial_search_query_count and max_research_loops // low means max 1 loop and 1 query @@ -127,13 +195,14 @@ export default function App() { } const newMessages: Message[] = [ - ...(thread.messages || []), + ...messages, { type: "human", content: submittedInputValue, id: Date.now().toString(), }, ]; + setMessages(newMessages); thread.submit({ messages: newMessages, initial_search_query_count: initial_search_query_count, @@ -141,48 +210,37 @@ export default function App() { reasoning_model: model, }); }, - [thread] + [thread, threadId, messages] ); const handleCancel = useCallback(() => { thread.stop(); - window.location.reload(); }, [thread]); return (
    +
    - {thread.messages.length === 0 ? ( - - ) : error ? ( -
    -
    -

    Error

    -

    {JSON.stringify(error)}

    - - -
    -
    - ) : ( - - )} + {messages.length === 0 ? ( + + ) : ( + + )}
    ); diff --git a/frontend/src/components/HistoryPanel.tsx b/frontend/src/components/HistoryPanel.tsx new file mode 100644 index 00000000..5d2ca28a --- /dev/null +++ b/frontend/src/components/HistoryPanel.tsx @@ -0,0 +1,32 @@ +import { Button } from "@/components/ui/button"; +import { ScrollArea } from "@/components/ui/scroll-area"; + +interface HistoryPanelProps { + history: any[]; + onSelectHistory: (historyItem: any) => void; +} + +export const HistoryPanel: React.FC = ({ + history, + onSelectHistory, +}) => { + return ( + + ); +}; diff --git a/frontend/src/components/InputForm.tsx b/frontend/src/components/InputForm.tsx index 97aa5c67..2e5e3162 100644 --- a/frontend/src/components/InputForm.tsx +++ b/frontend/src/components/InputForm.tsx @@ -25,7 +25,7 @@ export const InputForm: React.FC = ({ hasHistory, }) => { const [internalInputValue, setInternalInputValue] = useState(""); - const [effort, setEffort] = useState("medium"); + const [effort, setEffort] = useState("high"); const [model, setModel] = useState("gemini-2.5-flash-preview-04-17"); const handleInternalSubmit = (e?: React.FormEvent) => { diff --git a/install_deps.bat b/install_deps.bat new file mode 100644 index 00000000..e9f624ec --- /dev/null +++ b/install_deps.bat @@ -0,0 +1,48 @@ +@echo off +cd /d "%~dp0" + +echo. +echo ================================================================= +echo Installing Dependencies +echo ================================================================= +echo. + +REM 1. Create and install backend dependencies +if not exist "backend\venv\Scripts\activate.bat" ( + echo [INFO] Creating backend virtual environment... + py -3.11 -m venv backend\venv + if errorlevel 1 ( + echo [ERROR] Failed to create backend virtual environment. + pause + exit /b 1 + ) +) +echo [INFO] Installing backend dependencies... +call backend\venv\Scripts\activate.bat +pip install -e backend +if errorlevel 1 ( + echo [ERROR] Failed to install backend dependencies. + pause + exit /b 1 +) + +REM 2. Install frontend dependencies +if not exist "frontend\node_modules" ( + echo [INFO] Installing frontend dependencies... + pushd frontend + call npm install + if errorlevel 1 ( + echo [ERROR] Failed to install frontend dependencies. + popd + pause + exit /b 1 + ) + popd +) + +echo. +echo ================================================================= +echo Dependency installation complete. +echo ================================================================= +echo. +pause diff --git a/orig/App.tsx b/orig/App.tsx new file mode 100644 index 00000000..d06d4021 --- /dev/null +++ b/orig/App.tsx @@ -0,0 +1,189 @@ +import { useStream } from "@langchain/langgraph-sdk/react"; +import type { Message } from "@langchain/langgraph-sdk"; +import { useState, useEffect, useRef, useCallback } from "react"; +import { ProcessedEvent } from "@/components/ActivityTimeline"; +import { WelcomeScreen } from "@/components/WelcomeScreen"; +import { ChatMessagesView } from "@/components/ChatMessagesView"; +import { Button } from "@/components/ui/button"; + +export default function App() { + const [processedEventsTimeline, setProcessedEventsTimeline] = useState< + ProcessedEvent[] + >([]); + const [historicalActivities, setHistoricalActivities] = useState< + Record + >({}); + const scrollAreaRef = useRef(null); + const hasFinalizeEventOccurredRef = useRef(false); + const [error, setError] = useState(null); + const thread = useStream<{ + messages: Message[]; + initial_search_query_count: number; + max_research_loops: number; + reasoning_model: string; + }>({ + apiUrl: import.meta.env.DEV + ? "http://localhost:2024" + : "http://localhost:8123", + assistantId: "agent", + messagesKey: "messages", + onUpdateEvent: (event: any) => { + let processedEvent: ProcessedEvent | null = null; + if (event.generate_query) { + processedEvent = { + title: "Generating Search Queries", + data: event.generate_query?.search_query?.join(", ") || "", + }; + } else if (event.web_research) { + const sources = event.web_research.sources_gathered || []; + const numSources = sources.length; + const uniqueLabels = [ + ...new Set(sources.map((s: any) => s.label).filter(Boolean)), + ]; + const exampleLabels = uniqueLabels.slice(0, 3).join(", "); + processedEvent = { + title: "Web Research", + data: `Gathered ${numSources} sources. Related to: ${ + exampleLabels || "N/A" + }.`, + }; + } else if (event.reflection) { + processedEvent = { + title: "Reflection", + data: "Analysing Web Research Results", + }; + } else if (event.finalize_answer) { + processedEvent = { + title: "Finalizing Answer", + data: "Composing and presenting the final answer.", + }; + hasFinalizeEventOccurredRef.current = true; + } + if (processedEvent) { + setProcessedEventsTimeline((prevEvents) => [ + ...prevEvents, + processedEvent!, + ]); + } + }, + onError: (error: any) => { + setError(error.message); + }, + }); + + useEffect(() => { + if (scrollAreaRef.current) { + const scrollViewport = scrollAreaRef.current.querySelector( + "[data-radix-scroll-area-viewport]" + ); + if (scrollViewport) { + scrollViewport.scrollTop = scrollViewport.scrollHeight; + } + } + }, [thread.messages]); + + useEffect(() => { + if ( + hasFinalizeEventOccurredRef.current && + !thread.isLoading && + thread.messages.length > 0 + ) { + const lastMessage = thread.messages[thread.messages.length - 1]; + if (lastMessage && lastMessage.type === "ai" && lastMessage.id) { + setHistoricalActivities((prev) => ({ + ...prev, + [lastMessage.id!]: [...processedEventsTimeline], + })); + } + hasFinalizeEventOccurredRef.current = false; + } + }, [thread.messages, thread.isLoading, processedEventsTimeline]); + + const handleSubmit = useCallback( + (submittedInputValue: string, effort: string, model: string) => { + if (!submittedInputValue.trim()) return; + setProcessedEventsTimeline([]); + hasFinalizeEventOccurredRef.current = false; + + // convert effort to, initial_search_query_count and max_research_loops + // low means max 1 loop and 1 query + // medium means max 3 loops and 3 queries + // high means max 10 loops and 5 queries + let initial_search_query_count = 0; + let max_research_loops = 0; + switch (effort) { + case "low": + initial_search_query_count = 1; + max_research_loops = 1; + break; + case "medium": + initial_search_query_count = 3; + max_research_loops = 3; + break; + case "high": + initial_search_query_count = 5; + max_research_loops = 10; + break; + } + + const newMessages: Message[] = [ + ...(thread.messages || []), + { + type: "human", + content: submittedInputValue, + id: Date.now().toString(), + }, + ]; + thread.submit({ + messages: newMessages, + initial_search_query_count: initial_search_query_count, + max_research_loops: max_research_loops, + reasoning_model: model, + }); + }, + [thread] + ); + + const handleCancel = useCallback(() => { + thread.stop(); + window.location.reload(); + }, [thread]); + + return ( +
    +
    + {thread.messages.length === 0 ? ( + + ) : error ? ( +
    +
    +

    Error

    +

    {JSON.stringify(error)}

    + + +
    +
    + ) : ( + + )} +
    +
    + ); +} diff --git a/start_dev_servers.bat b/start_dev_servers.bat new file mode 100644 index 00000000..dfe8bf11 --- /dev/null +++ b/start_dev_servers.bat @@ -0,0 +1,21 @@ +@echo off +cd /d "%~dp0" + +if not exist "backend\venv\Scripts\activate.bat" ( + echo [ERROR] Backend dependencies not found. Please run install_deps.bat first. + pause + exit /b 1 +) + +if not exist "frontend\node_modules" ( + echo [ERROR] Frontend dependencies not found. Please run install_deps.bat first. + pause + exit /b 1 +) + +echo [INFO] Starting servers... +start "Backend" cmd /c "cd /d "%~dp0\backend" && .\venv\Scripts\activate.bat && langgraph dev" +start "Frontend" cmd /c "cd /d "%~dp0\frontend" && npm run dev" + +timeout /t 15 >nul +start http://localhost:5173/app/ \ No newline at end of file diff --git a/start_dev_servers_no_langsmith.bat b/start_dev_servers_no_langsmith.bat new file mode 100644 index 00000000..bbc6ae99 --- /dev/null +++ b/start_dev_servers_no_langsmith.bat @@ -0,0 +1,21 @@ +@echo off +cd /d "%~dp0" + +if not exist "backend\venv\Scripts\activate.bat" ( + echo [ERROR] Backend dependencies not found. Please run install_deps.bat first. + pause + exit /b 1 +) + +if not exist "frontend\node_modules" ( + echo [ERROR] Frontend dependencies not found. Please run install_deps.bat first. + pause + exit /b 1 +) + +echo [INFO] Starting servers... +start "Backend" cmd /c "cd /d "%~dp0\backend" && .\venv\Scripts\activate.bat && langgraph dev --no-browser" +start "Frontend" cmd /c "cd /d "%~dp0\frontend" && npm run dev" + +timeout /t 15 >nul +start http://localhost:5173/app/ \ No newline at end of file