├── agent ├── __init__.py ├── pyproject.toml ├── prompts.py ├── main.py ├── README.md └── stock_analysis.py ├── .DS_Store ├── frontend ├── postcss.config.mjs ├── src │ ├── app │ │ ├── favicon.ico │ │ ├── components │ │ │ ├── chart-components │ │ │ │ ├── section-title.tsx │ │ │ │ ├── insight-card.tsx │ │ │ │ ├── bar-chart.tsx │ │ │ │ ├── line-chart.tsx │ │ │ │ └── allocation-table.tsx │ │ │ ├── theme-provider.tsx │ │ │ ├── tool-logs.tsx │ │ │ ├── prompt-panel.tsx │ │ │ ├── component-tree.tsx │ │ │ ├── generative-canvas.tsx │ │ │ └── cash-panel.tsx │ │ ├── layout.tsx │ │ ├── globals.css │ │ ├── api │ │ │ └── copilotkit │ │ │ │ └── route.ts │ │ └── page.tsx │ └── utils │ │ └── prompts.ts ├── public │ ├── placeholder.jpg │ ├── placeholder-logo.png │ ├── placeholder-user.jpg │ ├── vercel.svg │ ├── window.svg │ ├── file.svg │ ├── globe.svg │ ├── next.svg │ ├── placeholder-logo.svg │ └── placeholder.svg ├── next.config.ts ├── .gitignore ├── tsconfig.json ├── package.json └── README.md ├── .gitignore └── README.md /agent/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheGreatBonnie/open-ag-ui-langgraph/HEAD/.DS_Store -------------------------------------------------------------------------------- /frontend/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /frontend/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheGreatBonnie/open-ag-ui-langgraph/HEAD/frontend/src/app/favicon.ico -------------------------------------------------------------------------------- /frontend/public/placeholder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheGreatBonnie/open-ag-ui-langgraph/HEAD/frontend/public/placeholder.jpg -------------------------------------------------------------------------------- /frontend/public/placeholder-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheGreatBonnie/open-ag-ui-langgraph/HEAD/frontend/public/placeholder-logo.png -------------------------------------------------------------------------------- /frontend/public/placeholder-user.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheGreatBonnie/open-ag-ui-langgraph/HEAD/frontend/public/placeholder-user.jpg -------------------------------------------------------------------------------- /frontend/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /frontend/src/app/components/chart-components/section-title.tsx: -------------------------------------------------------------------------------- 1 | interface SectionTitleProps { 2 | title: string 3 | } 4 | 5 | export function SectionTitle({ title }: SectionTitleProps) { 6 | return ( 7 |
8 |

{title}

9 |
10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /frontend/public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/app/components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | // 'use client' 2 | 3 | // import * as React from 'react' 4 | // import { 5 | // ThemeProvider as NextThemesProvider, 6 | // type ThemeProviderProps, 7 | // } from 'next-themes' 8 | 9 | // export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 10 | // return {children} 11 | // } 12 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@ag-ui/client": "^0.0.30", 13 | "@copilotkit/react-core": "^1.9.1", 14 | "@copilotkit/react-ui": "^1.9.1", 15 | "@copilotkit/runtime": "^1.9.1", 16 | "@copilotkit/runtime-client-gql": "^1.9.1", 17 | "lucide-react": "^0.525.0", 18 | "next": "15.3.4", 19 | "react": "^19.0.0", 20 | "react-dom": "^19.0.0", 21 | "recharts": "^3.0.2" 22 | }, 23 | "devDependencies": { 24 | "@tailwindcss/postcss": "^4", 25 | "@types/node": "^20", 26 | "@types/react": "^19", 27 | "@types/react-dom": "^19", 28 | "tailwindcss": "^4", 29 | "typescript": "^5" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /agent/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "agent" 3 | version = "0.1.0" 4 | description = "" 5 | authors = [ 6 | {name = "orca-copilotkit",email = "testapp@copilotkit.ai"} 7 | ] 8 | readme = "README.md" 9 | requires-python = ">=3.12,<3.13" 10 | dependencies = [ 11 | "copilotkit (>=0.1.52,<0.2.0)", 12 | "ag-ui-protocol (>=0.1.7,<0.2.0)", 13 | "fastapi (>=0.115.14,<0.116.0)", 14 | "uvicorn (>=0.35.0,<0.36.0)", 15 | "langchain-gemini (>=0.1.1,<0.2.0)", 16 | "yfinance (>=0.2.64,<0.3.0)", 17 | "langchain-openai (>=0.3.27,<0.4.0)", 18 | "dotenv (>=0.9.9,<0.10.0)", 19 | "pandas (>=2.3.0,<3.0.0)", 20 | "langchain[google-genai] (>=0.3.26,<0.4.0)", 21 | "lxml (>=6.0.0,<7.0.0)", 22 | "tavily-python (>=0.7.9,<0.8.0)" 23 | ] 24 | 25 | [tool.poetry] 26 | package-mode = false 27 | 28 | [build-system] 29 | requires = ["poetry-core>=2.0.0,<3.0.0"] 30 | build-backend = "poetry.core.masonry.api" 31 | -------------------------------------------------------------------------------- /frontend/public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Geist, Geist_Mono } from "next/font/google"; 3 | import "./globals.css"; 4 | import "@copilotkit/react-ui/styles.css"; 5 | import { CopilotKit } from "@copilotkit/react-core"; 6 | const geistSans = Geist({ 7 | variable: "--font-geist-sans", 8 | subsets: ["latin"], 9 | }); 10 | 11 | const geistMono = Geist_Mono({ 12 | variable: "--font-geist-mono", 13 | subsets: ["latin"], 14 | }); 15 | 16 | export const metadata: Metadata = { 17 | title: "AI Stock Portfolio", 18 | description: "AI Stock Portfolio", 19 | }; 20 | 21 | export default function RootLayout({ 22 | children, 23 | }: Readonly<{ 24 | children: React.ReactNode; 25 | }>) { 26 | return ( 27 | 28 | 30 | 31 | {children} 32 | 33 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /frontend/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | :root { 4 | --background: #ffffff; 5 | --foreground: #171717; 6 | } 7 | 8 | @theme inline { 9 | --color-background: var(--background); 10 | --color-foreground: var(--foreground); 11 | --font-sans: var(--font-geist-sans); 12 | --font-mono: var(--font-geist-mono); 13 | } 14 | 15 | @media (prefers-color-scheme: dark) { 16 | :root { 17 | --background: #0a0a0a; 18 | --foreground: #ededed; 19 | } 20 | } 21 | 22 | body { 23 | background: var(--background); 24 | color: var(--foreground); 25 | font-family: Arial, Helvetica, sans-serif; 26 | } 27 | 28 | /* Hide scrollbar for Chrome, Safari and Opera */ 29 | .hide-scrollbar::-webkit-scrollbar { 30 | display: none; 31 | } 32 | 33 | /* Hide scrollbar for IE, Edge and Firefox */ 34 | .hide-scrollbar { 35 | -ms-overflow-style: none; /* IE and Edge */ 36 | scrollbar-width: none; /* Firefox */ 37 | } 38 | 39 | /* .copilotKitMessagesContainer{ 40 | padding-top: 2px !important; 41 | padding: 0 !important; 42 | } 43 | 44 | .copilotKitMessages{ 45 | overflow-y: auto !important; 46 | } */ -------------------------------------------------------------------------------- /frontend/src/app/components/chart-components/insight-card.tsx: -------------------------------------------------------------------------------- 1 | interface Insight { 2 | title: string 3 | description: string 4 | emoji: string 5 | } 6 | 7 | interface InsightCardComponentProps { 8 | insight: Insight 9 | type: "bull" | "bear" 10 | } 11 | 12 | export function InsightCardComponent({ insight, type }: InsightCardComponentProps) { 13 | const getTypeStyles = () => { 14 | switch (type) { 15 | case "bull": 16 | return "border-l-4 border-l-[#00d237] bg-[#86ECE4]/10" 17 | case "bear": 18 | return "border-l-4 border-l-red-500 bg-red-50" 19 | default: 20 | return "border-l-4 border-l-[#D8D8E5]" 21 | } 22 | } 23 | 24 | return ( 25 |
26 |
27 | {insight.emoji} 28 |
29 |

{insight.title}

30 |

{insight.description}

31 |
32 |
33 |
34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /frontend/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /frontend/src/app/api/copilotkit/route.ts: -------------------------------------------------------------------------------- 1 | // Import the HttpAgent for making HTTP requests to the backend 2 | import { HttpAgent } from "@ag-ui/client"; 3 | 4 | // Import CopilotKit runtime components for setting up the API endpoint 5 | import { 6 | CopilotRuntime, 7 | ExperimentalEmptyAdapter, 8 | copilotRuntimeNextJSAppRouterEndpoint, 9 | GoogleGenerativeAIAdapter, 10 | } from "@copilotkit/runtime"; 11 | 12 | // Import NextRequest type for handling Next.js API requests 13 | import { NextRequest } from "next/server"; 14 | 15 | const serviceAdapter = new GoogleGenerativeAIAdapter(); 16 | 17 | // Create a new HttpAgent instance that connects to the LangGraph stock backend running locally 18 | const stockAgent = new HttpAgent({ 19 | url: "http://127.0.0.1:8000/langgraph-agent", 20 | }); 21 | 22 | // Initialize the CopilotKit runtime with our stock agent 23 | const runtime = new CopilotRuntime({ 24 | agents: { 25 | stockAgent, // Register the stock agent with the runtime 26 | }, 27 | }); 28 | 29 | /** 30 | * Define the POST handler for the API endpoint 31 | * This function handles incoming POST requests to the /api/copilotkit endpoint 32 | */ 33 | export const POST = async (req: NextRequest) => { 34 | // Configure the CopilotKit endpoint for the Next.js app router 35 | const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({ 36 | runtime, // Use the runtime with our research agent 37 | serviceAdapter, 38 | endpoint: "/api/copilotkit", // Define the API endpoint path 39 | }); 40 | 41 | // Process the incoming request with the CopilotKit handler 42 | return handleRequest(req); 43 | }; 44 | -------------------------------------------------------------------------------- /frontend/src/app/components/tool-logs.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { Check } from "lucide-react" 4 | import React, { useEffect } from "react" 5 | 6 | interface ToolLog { 7 | id: string | number 8 | message: string 9 | status: "processing" | "completed" 10 | } 11 | 12 | interface ToolLogsProps { 13 | logs: ToolLog[] 14 | } 15 | 16 | export function ToolLogs({ logs }: ToolLogsProps) { 17 | useEffect(() => { 18 | console.log(logs, "logs") 19 | }, []) 20 | return ( 21 |
22 | {logs.map((log) => ( 23 |
33 | {log.status === "processing" ? ( 34 | 35 | 36 | 37 | 38 | ) : ( 39 | 40 | )} 41 | {log.message} 42 |
43 | ))} 44 |
45 | ) 46 | } 47 | 48 | // Example usage (remove in production): 49 | // const sampleLogs = [ 50 | // { id: 1, message: "Fetching stock data...", status: "processing" }, 51 | // { id: 2, message: "Analysis complete!", status: "completed" }, 52 | // ] 53 | // 54 | -------------------------------------------------------------------------------- /frontend/src/app/components/prompt-panel.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import type React from "react" 4 | import { CopilotChat } from "@copilotkit/react-ui" 5 | 6 | 7 | interface PromptPanelProps { 8 | availableCash: number 9 | } 10 | 11 | 12 | 13 | export function PromptPanel({ availableCash }: PromptPanelProps) { 14 | 15 | 16 | const formatCurrency = (amount: number) => { 17 | return new Intl.NumberFormat("en-US", { 18 | style: "currency", 19 | currency: "USD", 20 | minimumFractionDigits: 0, 21 | maximumFractionDigits: 0, 22 | }).format(amount) 23 | } 24 | 25 | 26 | 27 | return ( 28 |
29 | {/* Header */} 30 |
31 |
32 | 🪁 33 |
34 |

Portfolio Chat

35 |
36 | PRO 37 |
38 |
39 |
40 |

Interact with the LangGraph-powered AI agent for portfolio visualization and analysis

41 | 42 | {/* Available Cash Display */} 43 |
44 |
Available Cash
45 |
{formatCurrency(availableCash)}
46 |
47 |
48 | 53 | 54 |
55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /frontend/src/app/components/chart-components/bar-chart.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts" 4 | 5 | interface BarChartData { 6 | ticker: string 7 | return: number 8 | } 9 | 10 | interface BarChartComponentProps { 11 | data: BarChartData[] 12 | size?: "normal" | "small" 13 | onClick?: (data: string) => void 14 | } 15 | 16 | export function BarChartComponent({ data, size = "normal", onClick }: BarChartComponentProps) { 17 | const height = size === "small" ? 80 : 160 // h-20 or h-40 18 | const padding = size === "small" ? "p-2" : "p-4" 19 | const fontSize = size === "small" ? 8 : 10 20 | const tooltipFontSize = size === "small" ? "9px" : "11px" 21 | return ( 22 |
23 |
24 | 25 | 26 | 27 | 28 | `${value}%`} 33 | /> 34 | [`${value.toFixed(1)}%`, "Return"]} 43 | /> 44 | { 45 | if (size === "normal") { 46 | // @ts-ignore 47 | console.log(data.payload, "clicked") 48 | // @ts-ignore 49 | onClick?.(data.payload.ticker as string) 50 | } 51 | }} dataKey="return" fill="#86ECE4" radius={[2, 2, 0, 0]} /> 52 | 53 | 54 |
55 |
56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /frontend/src/app/components/chart-components/line-chart.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts" 4 | 5 | interface LineChartData { 6 | date: string 7 | portfolio: number 8 | spy: number 9 | } 10 | 11 | interface LineChartComponentProps { 12 | data: LineChartData[] | [] | undefined 13 | size?: "normal" | "small" 14 | } 15 | 16 | export function LineChartComponent({ data, size = "normal" }: LineChartComponentProps) { 17 | const height = size === "small" ? 120 : 192 // h-30 or h-48 18 | const padding = size === "small" ? "p-2" : "p-4" 19 | const fontSize = size === "small" ? 8 : 10 20 | const tooltipFontSize = size === "small" ? "9px" : "11px" 21 | const legendFontSize = size === "small" ? "9px" : "11px" 22 | return ( 23 |
24 |
25 | 26 | 27 | 28 | 29 | `$${(value / 1000).toFixed(0)}K`} 34 | /> 35 | [ 45 | `$${value.toLocaleString()}`, 46 | name.toLowerCase() === "portfolio" ? "Portfolio" : "SPY", 47 | ]} 48 | /> 49 | 56 | 57 | 58 | 59 | 60 |
61 |
62 | ) 63 | } 64 | -------------------------------------------------------------------------------- /frontend/src/app/components/chart-components/allocation-table.tsx: -------------------------------------------------------------------------------- 1 | export interface Allocation { 2 | ticker: string 3 | allocation: number 4 | currentValue: number 5 | totalReturn: number 6 | } 7 | 8 | interface AllocationTableComponentProps { 9 | allocations: Allocation[] | [] | undefined 10 | size?: "normal" | "small" 11 | } 12 | 13 | export function AllocationTableComponent({ allocations, size = "normal" }: AllocationTableComponentProps) { 14 | // Define class variants based on size 15 | const padding = size === "small" ? "py-1 px-2" : "py-2 px-3" 16 | const fontSize = size === "small" ? "text-[10px]" : "text-xs" 17 | return ( 18 |
19 | 20 | 21 | 22 | 25 | 26 | 29 | 32 | 33 | 34 | 35 | {allocations?.map((allocation, index) => ( 36 | 37 | 40 | 41 | 44 | 50 | 51 | ))} 52 | 53 |
23 | Ticker 24 | % 27 | Value 28 | 30 | Return 31 |
38 | {allocation.ticker} 39 | {allocation.allocation.toFixed(2)}% 42 | ${(allocation.currentValue / 1000).toFixed(1)}K 43 | 45 | = 0 ? "text-[#1B606F]" : "text-red-600"}> 46 | {allocation.totalReturn >= 0 ? "+" : ""} 47 | {allocation.totalReturn.toFixed(1)}% 48 | 49 |
54 |
55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /frontend/src/utils/prompts.ts: -------------------------------------------------------------------------------- 1 | export const INVESTMENT_SUGGESTION_PROMPT = ` 2 | You are an AI assistant that generates investment suggestions for a stock portfolio interface. Your role is to provide actionable investment recommendations based on the current portfolio state. 3 | 4 | CONTEXT RULES: 5 | - Investment suggestions should reference historical entry points (not future predictions) 6 | - Investment period must not exceed 4 years from the suggested entry date 7 | - Use realistic dollar amounts typically ranging from $5,000 to $50,000 per stock 8 | - Focus on well-known, publicly traded companies with strong historical performance 9 | - Entry dates should be from 2020 onwards but at least 6 months in the past from current date 10 | 11 | PORTFOLIO STATE CONDITIONS: 12 | 13 | WHEN PORTFOLIO IS EMPTY: 14 | Generate initial investment suggestions that establish a diversified portfolio. Format as complete investment recommendations with specific dollar amounts and historical entry points. 15 | 16 | FORMAT EXAMPLES: 17 | - "Invest $15,000 in Apple and $20,000 in Microsoft since January 2021" 18 | - "Put $12,000 in Tesla and $18,000 in Amazon starting from March 2022" 19 | - "Allocate $25,000 to Nvidia and $10,000 to Google from September 2020" 20 | 21 | WHEN PORTFOLIO HAS EXISTING HOLDINGS: 22 | Generate suggestions to modify the current portfolio through additions, removals, or replacements. Reference the existing holdings when making recommendations. 23 | 24 | ADDITION FORMAT EXAMPLES: 25 | - "Add $15,000 worth of Meta stocks to your portfolio" 26 | - "Consider adding $20,000 in Berkshire Hathaway to diversify your holdings" 27 | 28 | REPLACEMENT FORMAT EXAMPLES: 29 | - "Replace your Tesla position with Toyota using the same investment amount" 30 | - "Swap your current Apple shares for Microsoft with equivalent dollar value" 31 | 32 | REMOVAL FORMAT EXAMPLES: 33 | - "Consider reducing your Nvidia position by $10,000 and reallocating to bonds" 34 | - "Remove your Amazon holdings and redistribute the funds across your other positions" 35 | 36 | GUIDELINES: 37 | - Keep suggestions concise and actionable (1-2 sentences max) 38 | - Use specific dollar amounts, not percentages 39 | - Reference realistic historical timeframes 40 | - Avoid giving financial advice disclaimers in the suggestions 41 | - Focus on major market cap stocks for reliability 42 | - Always provide a single investment date for multiple stocks 43 | - Consider sector diversification when making recommendations 44 | 45 | OUTPUT FORMAT: 46 | Generate 3-5 distinct suggestions per request, each as a separate, actionable statement that can be displayed as clickable options in the UI. 47 | 48 | Current portfolio state: {{portfolioState}} 49 | Portfolio holdings: {{portfolioHoldings}} 50 | `; 51 | 52 | export const initialMessage = "Hi, I am a stock agent. I can help you analyze and compare different stocks. Please ask me anything about the stock market.\n\nI have capabilities to fetch historical data of stocks, revenue, and also compare companies performances." -------------------------------------------------------------------------------- /frontend/public/placeholder-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/placeholder.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/app/components/component-tree.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { ChevronDown, ChevronRight } from "lucide-react" 4 | import { useState } from "react" 5 | import type { PortfolioState } from "@/app/page" 6 | 7 | interface ComponentTreeProps { 8 | portfolioState: PortfolioState 9 | } 10 | 11 | interface TreeNode { 12 | id: string 13 | name: string 14 | type: string 15 | children?: TreeNode[] 16 | } 17 | 18 | export function ComponentTree({ portfolioState }: ComponentTreeProps) { 19 | const [expandedNodes, setExpandedNodes] = useState>( 20 | new Set(["root", "performance", "allocation-returns", "insights"]), 21 | ) 22 | 23 | const toggleNode = (nodeId: string) => { 24 | const newExpanded = new Set(expandedNodes) 25 | if (newExpanded.has(nodeId)) { 26 | newExpanded.delete(nodeId) 27 | } else { 28 | newExpanded.add(nodeId) 29 | } 30 | setExpandedNodes(newExpanded) 31 | } 32 | 33 | const treeData: TreeNode = { 34 | id: "root", 35 | name: "Canvas", 36 | type: "Container", 37 | children: [ 38 | { 39 | id: "performance", 40 | name: "Performance", 41 | type: "Section", 42 | children: [{ id: "line-chart", name: "Line Chart", type: "Chart" }], 43 | }, 44 | { 45 | id: "allocation-returns", 46 | name: "Data Grid", 47 | type: "Grid", 48 | children: [ 49 | { id: "alloc-table", name: "Allocation", type: "Table" }, 50 | { id: "bar-chart", name: "Returns", type: "Chart" }, 51 | ], 52 | }, 53 | { 54 | id: "insights", 55 | name: "Market Insights", 56 | type: "Section", 57 | children: [ 58 | { 59 | id: "bull-section", 60 | name: "Bull Case", 61 | type: "Column", 62 | children: portfolioState.bullInsights.map((_, index) => ({ 63 | id: `bull-${index}`, 64 | name: `Bull Insight ${index + 1}`, 65 | type: "Card", 66 | })), 67 | }, 68 | { 69 | id: "bear-section", 70 | name: "Bear Case", 71 | type: "Column", 72 | children: portfolioState.bearInsights.map((_, index) => ({ 73 | id: `bear-${index}`, 74 | name: `Bear Insight ${index + 1}`, 75 | type: "Card", 76 | })), 77 | }, 78 | ], 79 | }, 80 | ], 81 | } 82 | 83 | const renderNode = (node: TreeNode, depth = 0) => { 84 | const isExpanded = expandedNodes.has(node.id) 85 | const hasChildren = node.children && node.children.length > 0 86 | 87 | return ( 88 |
89 |
hasChildren && toggleNode(node.id)} 93 | > 94 | {hasChildren ? ( 95 | isExpanded ? ( 96 | 97 | ) : ( 98 | 99 | ) 100 | ) : ( 101 |
102 | )} 103 | {node.name} 104 | ({node.type}) 105 |
106 | {hasChildren && isExpanded &&
{node.children!.map((child) => renderNode(child, depth + 1))}
} 107 |
108 | ) 109 | } 110 | 111 | return ( 112 |
113 |
114 |

Component Tree

115 |

Layout structure

116 |
117 | 118 |
{renderNode(treeData)}
119 | 120 |
121 |
122 |
Components: {portfolioState.bullInsights.length + portfolioState.bearInsights.length + 5}
123 |
Updated: {new Date().toLocaleTimeString()}
124 |
125 |
126 |
127 | ) 128 | } 129 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[codz] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | #poetry.toml 110 | 111 | # pdm 112 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 113 | # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. 114 | # https://pdm-project.org/en/latest/usage/project/#working-with-version-control 115 | #pdm.lock 116 | #pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # pixi 121 | # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. 122 | #pixi.lock 123 | # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one 124 | # in the .venv directory. It is recommended not to include this directory in version control. 125 | .pixi 126 | 127 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 128 | __pypackages__/ 129 | 130 | # Celery stuff 131 | celerybeat-schedule 132 | celerybeat.pid 133 | 134 | # SageMath parsed files 135 | *.sage.py 136 | 137 | # Environments 138 | .env 139 | .envrc 140 | .venv 141 | env/ 142 | venv/ 143 | ENV/ 144 | env.bak/ 145 | venv.bak/ 146 | 147 | # Spyder project settings 148 | .spyderproject 149 | .spyproject 150 | 151 | # Rope project settings 152 | .ropeproject 153 | 154 | # mkdocs documentation 155 | /site 156 | 157 | # mypy 158 | .mypy_cache/ 159 | .dmypy.json 160 | dmypy.json 161 | 162 | # Pyre type checker 163 | .pyre/ 164 | 165 | # pytype static type analyzer 166 | .pytype/ 167 | 168 | # Cython debug symbols 169 | cython_debug/ 170 | 171 | # PyCharm 172 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 173 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 174 | # and can be added to the global gitignore or merged into this file. For a more nuclear 175 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 176 | #.idea/ 177 | 178 | # Abstra 179 | # Abstra is an AI-powered process automation framework. 180 | # Ignore directories containing user credentials, local state, and settings. 181 | # Learn more at https://abstra.io/docs 182 | .abstra/ 183 | 184 | # Visual Studio Code 185 | # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore 186 | # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore 187 | # and can be added to the global gitignore or merged into this file. However, if you prefer, 188 | # you could uncomment the following to ignore the entire vscode folder 189 | # .vscode/ 190 | 191 | # Ruff stuff: 192 | .ruff_cache/ 193 | 194 | # PyPI configuration file 195 | .pypirc 196 | 197 | # Cursor 198 | # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to 199 | # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data 200 | # refer to https://docs.cursor.com/context/ignore-files 201 | .cursorignore 202 | .cursorindexingignore 203 | 204 | # Marimo 205 | marimo/_static/ 206 | marimo/_lsp/ 207 | __marimo__/ 208 | -------------------------------------------------------------------------------- /frontend/src/app/components/generative-canvas.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { LineChartComponent } from "./chart-components/line-chart" 4 | import { AllocationTableComponent } from "./chart-components/allocation-table" 5 | import { InsightCardComponent } from "./chart-components/insight-card" 6 | import { SectionTitle } from "./chart-components/section-title" 7 | import { BarChartComponent } from "./chart-components/bar-chart" 8 | import type { PortfolioState, SandBoxPortfolioState } from "../page" 9 | 10 | interface GenerativeCanvasProps { 11 | portfolioState: PortfolioState 12 | setSelectedStock: (stock: string) => void 13 | sandBoxPortfolio: SandBoxPortfolioState[] 14 | setSandBoxPortfolio: (portfolio: SandBoxPortfolioState[]) => void 15 | } 16 | 17 | export function GenerativeCanvas({ portfolioState, setSelectedStock, sandBoxPortfolio, setSandBoxPortfolio }: GenerativeCanvasProps) { 18 | return ( 19 |
20 |
21 | {/* Performance Section */} 22 |
23 | 24 |
25 | {portfolioState?.performanceData?.length === 0 ? ( 26 |
No performance data to show.
27 | ) : ( 28 | ({ 31 | ...d, 32 | portfolio: d.portfolio ?? 0, 33 | spy: d.spy ?? 0, 34 | })) 35 | } 36 | /> 37 | )} 38 |
39 |
40 | 41 | {/* Allocation and Returns Section */} 42 |
43 |
44 | 45 |
46 | {portfolioState.allocations.length === 0 ? ( 47 |
No allocation data to show.
48 | ) : ( 49 | ({ 52 | ...a, 53 | allocation: Number(a.allocation), 54 | })) 55 | } 56 | /> 57 | )} 58 |
59 |
60 | 61 |
62 | 63 |
64 | {portfolioState.returnsData.length === 0 ? ( 65 |
No returns data to show.
66 | ) : ( 67 | 68 | )} 69 |
70 |
71 |
72 | 73 | {/* Insights Section */} 74 |
75 | 76 |
77 | {/* Bull Insights */} 78 |
79 |
80 | 🐂 81 |

BULL CASE

82 |
83 |
84 | {portfolioState.bullInsights.length === 0 ? ( 85 |
No bull case insights.
86 | ) : ( 87 | portfolioState.bullInsights.map((insight, index) => ( 88 | 89 | )) 90 | )} 91 |
92 |
93 | 94 | {/* Bear Insights */} 95 |
96 |
97 | 🐻 98 |

BEAR CASE

99 |
100 |
101 | {portfolioState.bearInsights.length === 0 ? ( 102 |
No bear case insights.
103 | ) : ( 104 | portfolioState.bearInsights.map((insight, index) => ( 105 | 106 | )) 107 | )} 108 |
109 |
110 |
111 |
112 | 113 | {/* Custom Charts */} 114 | 132 |
133 |
134 | ) 135 | } 136 | -------------------------------------------------------------------------------- /agent/prompts.py: -------------------------------------------------------------------------------- 1 | system_prompt = """ 2 | You are a specialized stock portfolio analysis agent designed to help users analyze investment opportunities and track stock performance over time. Your primary role is to process investment queries and provide comprehensive analysis using available tools and data. 3 | 4 | CORE RESPONSIBILITIES: 5 | 6 | Investment Analysis: 7 | - Analyze stock performance for specified time periods 8 | - Calculate investment returns and portfolio growth 9 | - Provide historical price data and trends 10 | - Generate visualizations of stock performance when helpful 11 | 12 | Query Processing: 13 | - Process investment queries like "Invest in Apple with 10k dollars since Jan 2023" or "Make investments in Apple since 2021" 14 | - Extract key information: stock symbol, investment amount, time period 15 | - Work with available data without requesting additional clarification 16 | - Assume reasonable defaults when specific details are missing 17 | 18 | Portfolio Data Context: 19 | - Use the provided portfolio data as the primary reference for current holdings 20 | - Portfolio data contains a list of tickers and their invested amounts 21 | - Prioritize portfolio context over previous message history when analyzing investments 22 | - When analyzing portfolio performance, reference the provided portfolio data rather than searching through conversation history 23 | 24 | PORTFOLIO DATA: 25 | {PORTFOLIO_DATA_PLACEHOLDER} 26 | 27 | The portfolio data above is provided in JSON format containing the current holdings with tickers and their respective investment amounts. Use this data as the authoritative source for all portfolio-related queries and analysis. 28 | 29 | CRITICAL PORTFOLIO MANAGEMENT RULES: 30 | 31 | Investment Query Behavior: 32 | - DEFAULT ACTION: All investment queries (e.g., "Invest in Apple", "Make investments in Apple", "Add Apple to portfolio") should ADD TO the existing portfolio, not replace it 33 | - ADDITIVE APPROACH: When processing investment queries, always combine new investments with existing holdings 34 | - PORTFOLIO PRESERVATION: Never remove or replace existing portfolio holdings unless explicitly requested with clear removal language 35 | 36 | Portfolio Modification Guidelines: 37 | - ADD: Queries like "Invest in [stock]", "Make investments in [stock]", "Add [stock]" = ADD to existing portfolio 38 | - REMOVE: Only remove stocks when explicitly stated: "Remove [stock]", "Sell [stock]", "Drop [stock] from portfolio" 39 | - REPLACE: Only replace entire portfolio when explicitly stated: "Replace portfolio with [stocks]", "Clear portfolio and invest in [stocks]" 40 | 41 | Tool Utilization: 42 | - Use available tools proactively to gather stock data 43 | - When using extract_relevant_data_from_user_prompt tool, make sure that you are using it one time with multiple tickers and not multiple times with single ticker. 44 | - For portfolio modification queries (add/remove/replace stocks), when using extract_relevant_data_from_user_prompt tool: 45 | * For ADD operations: Return the complete updated list including ALL existing tickers from portfolio context PLUS the newly added tickers 46 | * For REMOVE operations: Return the complete updated list with specified tickers removed from the existing portfolio 47 | * For REPLACE operations: Return only the new tickers specified for replacement 48 | - Fetch historical price information 49 | - Calculate returns and performance metrics 50 | - Generate charts and visualizations when appropriate 51 | 52 | BEHAVIORAL GUIDELINES: 53 | 54 | Minimal Questions Approach: 55 | - Do NOT ask multiple clarifying questions - work with the information provided 56 | - If a stock symbol is unclear, make reasonable assumptions or use the most likely match 57 | - Use standard date formats and assume current date if end date not specified 58 | - Default to common investment scenarios when details are ambiguous 59 | 60 | Data Processing Rules: 61 | - Extract stock symbols from company names automatically 62 | - Handle date ranges flexibly (e.g., "since Jan 2023" means January 1, 2023 to present) 63 | - Calculate returns using closing prices 64 | - Account for stock splits and dividends when data is available 65 | - When portfolio data is provided, use it as the authoritative source for current holdings and investment amounts 66 | 67 | Context Priority: 68 | - Portfolio data context takes precedence over conversation history 69 | - Use portfolio data to understand current holdings without needing to reference previous messages 70 | - Process queries efficiently by relying on the provided portfolio context rather than parsing lengthy message arrays 71 | 72 | EXAMPLE PROCESSING FLOW: 73 | 74 | For a query like "Invest in Apple with 10k dollars since Jan 2023" or "Make investments in Apple since 2021": 75 | 1. Extract parameters: AAPL, $10,000, Jan 1 2023 - present 76 | 2. IMPORTANT: Combine with existing portfolio (ADD operation, not replace) 77 | 3. Fetch data: Get historical AAPL prices for the period 78 | 4. Calculate: Shares purchased, current value, total return 79 | 5. Present: Clear summary with performance metrics and context 80 | 6. Show updated portfolio composition including both existing holdings and new addition 81 | 82 | For portfolio analysis queries: 83 | 1. Reference provided portfolio data for current holdings 84 | 2. Extract relevant tickers and investment amounts from portfolio context 85 | 3. Fetch historical data for portfolio holdings 86 | 4. Calculate overall portfolio performance and individual stock contributions 87 | 5. Present comprehensive portfolio analysis 88 | 89 | RESPONSE FORMAT: 90 | 91 | Structure your responses as: 92 | - Investment Summary: Initial investment, current value, total return 93 | - Performance Analysis: Key metrics, percentage gains/losses 94 | - Timeline Context: Major events or trends during the period 95 | - Portfolio Impact: How the new investment affects overall portfolio composition 96 | - Visual Elements: Charts or graphs when helpful for understanding 97 | - When using markdown, use only basic text and bullet points. Do not use any other markdown elements. 98 | 99 | KEY CONSTRAINTS: 100 | - Work autonomously with provided information 101 | - Minimize back-and-forth questions 102 | - Focus on actionable analysis over theoretical discussion 103 | - Use tools efficiently to gather necessary data 104 | - Provide concrete numbers and specific timeframes 105 | - Assume user wants comprehensive analysis, not just basic data 106 | - Prioritize portfolio context data over conversation history for efficiency 107 | - ALWAYS default to additive portfolio management unless explicitly told otherwise 108 | 109 | Remember: Your goal is to provide immediate, useful investment analysis that helps users understand how their hypothetical or actual investments would have performed over specified time periods. When portfolio data is provided as context, use it as the primary source of truth for current holdings and investment amounts. By default, all investment queries should ADD to the existing portfolio, preserving existing holdings while incorporating new investments. Always respond with a valid content. 110 | """ 111 | 112 | insights_prompt =""" 113 | You are a financial news analysis assistant specialized in processing stock market news and sentiment analysis. User will provide a list of tickers and you will generate insights for each ticker. YOu must always use the tool provided to generate your insights. User might give multiple tickers at once. But only use the tool once and provide all the args in a single tool call. 114 | """ -------------------------------------------------------------------------------- /frontend/src/app/components/cash-panel.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState } from "react" 4 | import { Edit2, Check, X, DollarSign, TrendingUp, Wallet, Calendar } from "lucide-react" 5 | 6 | interface CashPanelProps { 7 | totalCash: number 8 | investedAmount: number 9 | currentPortfolioValue: number 10 | onTotalCashChange: (amount: number) => void 11 | onStateCashChange: (state: any) => void 12 | } 13 | 14 | export function CashPanel({ totalCash, investedAmount, currentPortfolioValue, onTotalCashChange, onStateCashChange }: CashPanelProps) { 15 | const [isEditing, setIsEditing] = useState(false) 16 | const [editValue, setEditValue] = useState(totalCash.toString()) 17 | 18 | // const availableCash = totalCash - investedAmount 19 | const investedPercentage = totalCash > 0 ? (investedAmount / (totalCash + investedAmount)) * 100 : 0 20 | const fourYearReturn = currentPortfolioValue - investedAmount - totalCash 21 | const fourYearReturnPercentage = investedAmount > 0 ? (fourYearReturn / investedAmount) * 100 : 0 22 | 23 | const handleEdit = () => { 24 | setIsEditing(true) 25 | setEditValue(totalCash.toString()) 26 | } 27 | 28 | const handleSave = () => { 29 | const newAmount = Number.parseInt(editValue.replace(/,/g, "")) 30 | if (!isNaN(newAmount) && newAmount >= 0) { 31 | onTotalCashChange(newAmount) 32 | onStateCashChange((prevState: any) => ({ 33 | ...prevState, 34 | available_cash: newAmount, 35 | })) 36 | } 37 | setIsEditing(false) 38 | } 39 | 40 | const handleCancel = () => { 41 | setIsEditing(false) 42 | setEditValue(totalCash.toString()) 43 | } 44 | 45 | const formatCurrency = (amount: number) => { 46 | return new Intl.NumberFormat("en-US", { 47 | style: "currency", 48 | currency: "USD", 49 | minimumFractionDigits: 0, 50 | maximumFractionDigits: 0, 51 | }).format(amount) 52 | } 53 | 54 | return ( 55 |
56 |
57 | {/* Total Cash */} 58 |
59 |
60 | 61 |
62 |
63 |
Total Cash
64 | {isEditing ? ( 65 |
66 | setEditValue(e.target.value)} 70 | className="w-12 text-sm font-semibold text-[#030507] font-['Roobert']" 71 | onKeyDown={(e) => e.key === "Enter" && handleSave()} 72 | /> 73 | 76 | 79 |
80 | ) : ( 81 |
82 | 83 | {formatCurrency(totalCash)} 84 | 85 | 91 |
92 | )} 93 |
94 |
95 | 96 | {/* Invested Amount */} 97 |
98 |
99 | 100 |
101 |
102 |
Invested
103 |
104 | {formatCurrency(investedAmount)} 105 |
106 |
107 |
108 | 109 | {/* Current Portfolio Value */} 110 |
111 |
112 | 113 |
114 |
115 |
Portfolio Value
116 |
117 | {formatCurrency(currentPortfolioValue)} 118 |
119 |
120 |
121 | 122 | {/* 4-Year Return */} 123 |
124 |
= 0 ? "bg-[#86ECE4]" : "bg-red-100" 127 | }`} 128 | > 129 | = 0 ? "text-[#030507]" : "text-red-600"} /> 130 |
131 |
132 |
4-Year Return
133 |
134 |
= 0 ? "text-[#1B606F]" : "text-red-600" 137 | }`} 138 | > 139 | {fourYearReturn >= 0 ? "+" : ""} 140 | {formatCurrency(fourYearReturn)} 141 |
142 |
= 0 ? "text-[#1B606F]" : "text-red-600"}`}> 143 | ({fourYearReturn >= 0 ? "+" : ""} 144 | {fourYearReturnPercentage.toFixed(1)}%) 145 |
146 |
147 |
148 |
149 | 150 | {/* Available Cash */} 151 | {/*
152 |
153 | 154 |
155 |
156 |
Available
157 |
162 | {formatCurrency(availableCash)} 163 |
164 |
165 |
*/} 166 |
167 | 168 | {/* Investment Progress */} 169 |
170 |
171 |
Portfolio Allocation
172 |
{investedPercentage.toFixed(1)}%
173 |
174 |
175 |
179 |
180 |
181 |
182 | ) 183 | } 184 | -------------------------------------------------------------------------------- /agent/main.py: -------------------------------------------------------------------------------- 1 | # Import necessary libraries and modules 2 | from fastapi import FastAPI 3 | from fastapi.responses import StreamingResponse # For streaming server-sent events 4 | import uuid # For generating unique message IDs 5 | from typing import Any # For type hints 6 | import os # For environment variables 7 | import uvicorn # ASGI server for running the FastAPI app 8 | import asyncio # For asynchronous programming 9 | 10 | # Import AG UI core components for event-driven communication 11 | from ag_ui.core import ( 12 | RunAgentInput, # Input data structure for agent runs 13 | StateSnapshotEvent, # Event for capturing state snapshots 14 | EventType, # Enumeration of all event types 15 | RunStartedEvent, # Event to signal run start 16 | RunFinishedEvent, # Event to signal run completion 17 | TextMessageStartEvent, # Event to start text message streaming 18 | TextMessageEndEvent, # Event to end text message streaming 19 | TextMessageContentEvent, # Event for text message content chunks 20 | ToolCallStartEvent, # Event to start tool call 21 | ToolCallEndEvent, # Event to end tool call 22 | ToolCallArgsEvent, # Event for tool call arguments 23 | StateDeltaEvent # Event for state changes 24 | ) 25 | from ag_ui.encoder import EventEncoder # Encoder for converting events to SSE format 26 | from stock_analysis import agent_graph # Import the LangGraph agent 27 | from copilotkit import CopilotKitState # Base state class from CopilotKit 28 | 29 | # Initialize FastAPI application instance 30 | app = FastAPI() 31 | 32 | 33 | class AgentState(CopilotKitState): 34 | """ 35 | AgentState defines the structure of data that flows through the agent. 36 | It extends CopilotKitState and contains all the information needed 37 | for stock analysis and investment operations. 38 | """ 39 | 40 | # List of available tools for the agent to use 41 | tools: list 42 | # Conversation history between user and assistant 43 | messages: list 44 | # Stock data retrieved from backend APIs 45 | be_stock_data: Any 46 | # Arguments passed to backend functions 47 | be_arguments: dict 48 | # Amount of cash available for investment 49 | available_cash: int 50 | # Summary of current investments 51 | investment_summary: dict 52 | # Log of tool executions and their results 53 | tool_logs: list 54 | 55 | # FastAPI endpoint that handles agent execution requests 56 | @app.post("/langgraph-agent") 57 | async def langgraph_agent(input_data: RunAgentInput): 58 | """ 59 | Main endpoint that processes agent requests and streams back events. 60 | 61 | Args: 62 | input_data (RunAgentInput): Contains thread_id, run_id, messages, tools, and state 63 | 64 | Returns: 65 | StreamingResponse: Server-sent events stream with agent execution updates 66 | """ 67 | try: 68 | # Define async generator function to produce server-sent events 69 | async def event_generator(): 70 | # Step 1: Initialize event encoding and communication infrastructure 71 | encoder = EventEncoder() # Converts events to SSE format 72 | event_queue = asyncio.Queue() # Queue for events from agent execution 73 | 74 | # Helper function to add events to the queue 75 | def emit_event(event): 76 | event_queue.put_nowait(event) 77 | 78 | # Generate unique identifier for this message thread 79 | message_id = str(uuid.uuid4()) 80 | 81 | # Step 2: Signal the start of agent execution 82 | yield encoder.encode( 83 | RunStartedEvent( 84 | type=EventType.RUN_STARTED, 85 | thread_id=input_data.thread_id, 86 | run_id=input_data.run_id, 87 | ) 88 | ) 89 | 90 | # Step 3: Send initial state snapshot to frontend 91 | yield encoder.encode( 92 | StateSnapshotEvent( 93 | type=EventType.STATE_SNAPSHOT, 94 | snapshot={ 95 | "available_cash": input_data.state["available_cash"], 96 | "investment_summary": input_data.state["investment_summary"], 97 | "investment_portfolio": input_data.state["investment_portfolio"], 98 | "tool_logs": [] 99 | } 100 | ) 101 | ) 102 | 103 | # Step 4: Initialize agent state with input data 104 | state = AgentState( 105 | tools=input_data.tools, 106 | messages=input_data.messages, 107 | be_stock_data=None, # Will be populated by agent tools 108 | be_arguments=None, # Will be populated by agent tools 109 | available_cash=input_data.state["available_cash"], 110 | investment_portfolio=input_data.state["investment_portfolio"], 111 | tool_logs=[] 112 | ) 113 | 114 | # Step 5: Create and configure the LangGraph agent 115 | agent = await agent_graph() 116 | 117 | # Step 6: Start agent execution asynchronously 118 | agent_task = asyncio.create_task( 119 | agent.ainvoke( 120 | state, config={"emit_event": emit_event, "message_id": message_id} 121 | ) 122 | ) 123 | 124 | # Step 7: Stream events from agent execution as they occur 125 | while True: 126 | try: 127 | # Wait for events with short timeout to check if agent is done 128 | event = await asyncio.wait_for(event_queue.get(), timeout=0.1) 129 | yield encoder.encode(event) 130 | except asyncio.TimeoutError: 131 | # Check if the agent execution has completed 132 | if agent_task.done(): 133 | break 134 | 135 | # Step 8: Clear tool logs after execution 136 | yield encoder.encode( 137 | StateDeltaEvent( 138 | type=EventType.STATE_DELTA, 139 | delta=[ 140 | { 141 | "op": "replace", 142 | "path": "/tool_logs", 143 | "value": [] 144 | } 145 | ] 146 | ) 147 | ) 148 | # Step 9: Handle the final message from the agent 149 | if state["messages"][-1].role == "assistant": 150 | # Check if the assistant made tool calls 151 | if state["messages"][-1].tool_calls: 152 | # Step 9a: Stream tool call events if tools were used 153 | 154 | # Signal the start of tool execution 155 | yield encoder.encode( 156 | ToolCallStartEvent( 157 | type=EventType.TOOL_CALL_START, 158 | tool_call_id=state["messages"][-1].tool_calls[0].id, 159 | toolCallName=state["messages"][-1] 160 | .tool_calls[0] 161 | .function.name, 162 | ) 163 | ) 164 | 165 | # Send the tool call arguments 166 | yield encoder.encode( 167 | ToolCallArgsEvent( 168 | type=EventType.TOOL_CALL_ARGS, 169 | tool_call_id=state["messages"][-1].tool_calls[0].id, 170 | delta=state["messages"][-1] 171 | .tool_calls[0] 172 | .function.arguments, 173 | ) 174 | ) 175 | 176 | # Signal the end of tool execution 177 | yield encoder.encode( 178 | ToolCallEndEvent( 179 | type=EventType.TOOL_CALL_END, 180 | tool_call_id=state["messages"][-1].tool_calls[0].id, 181 | ) 182 | ) 183 | else: 184 | # Step 9b: Stream text message if no tools were used 185 | 186 | # Signal the start of text message 187 | yield encoder.encode( 188 | TextMessageStartEvent( 189 | type=EventType.TEXT_MESSAGE_START, 190 | message_id=message_id, 191 | role="assistant", 192 | ) 193 | ) 194 | 195 | # Stream the message content in chunks for better UX 196 | if state["messages"][-1].content: 197 | content = state["messages"][-1].content 198 | 199 | # Split content into 5 parts for gradual streaming 200 | n_parts = 5 201 | part_length = max(1, len(content) // n_parts) 202 | parts = [content[i:i+part_length] for i in range(0, len(content), part_length)] 203 | 204 | # Handle rounding by merging extra parts into the last one 205 | if len(parts) > n_parts: 206 | parts = parts[:n_parts-1] + [''.join(parts[n_parts-1:])] 207 | 208 | # Stream each part with a delay for typing effect 209 | for part in parts: 210 | yield encoder.encode( 211 | TextMessageContentEvent( 212 | type=EventType.TEXT_MESSAGE_CONTENT, 213 | message_id=message_id, 214 | delta=part, 215 | ) 216 | ) 217 | await asyncio.sleep(0.5) # 500ms delay between chunks 218 | else: 219 | # Send error message if content is empty 220 | yield encoder.encode( 221 | TextMessageContentEvent( 222 | type=EventType.TEXT_MESSAGE_CONTENT, 223 | message_id=message_id, 224 | delta="Something went wrong! Please try again.", 225 | ) 226 | ) 227 | 228 | # Signal the end of text message 229 | yield encoder.encode( 230 | TextMessageEndEvent( 231 | type=EventType.TEXT_MESSAGE_END, 232 | message_id=message_id, 233 | ) 234 | ) 235 | 236 | # Step 10: Signal the completion of the entire agent run 237 | yield encoder.encode( 238 | RunFinishedEvent( 239 | type=EventType.RUN_FINISHED, 240 | thread_id=input_data.thread_id, 241 | run_id=input_data.run_id, 242 | ) 243 | ) 244 | 245 | except Exception as e: 246 | # Log any errors that occur during execution 247 | print(e) 248 | 249 | # Return the event generator as a streaming response 250 | return StreamingResponse(event_generator(), media_type="text/event-stream") 251 | 252 | 253 | def main(): 254 | """ 255 | Main function to start the FastAPI server. 256 | 257 | This function: 258 | 1. Gets the port from environment variable PORT (defaults to 8000) 259 | 2. Starts the uvicorn ASGI server 260 | 3. Enables hot reload for development 261 | """ 262 | # Get port from environment variable, default to 8000 263 | port = int(os.getenv("PORT", "8000")) 264 | 265 | # Start the uvicorn server with the FastAPI app 266 | uvicorn.run( 267 | "main:app", # Reference to the FastAPI app instance 268 | host="0.0.0.0", # Listen on all available interfaces 269 | port=port, # Use the configured port 270 | reload=True, # Enable auto-reload for development 271 | ) 272 | 273 | 274 | # Entry point: run the server when script is executed directly 275 | if __name__ == "__main__": 276 | main() 277 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Open AG UI Demo 2 | 3 | A full-stack AI-powered stock analysis and portfolio management application that demonstrates the integration of CopilotKit with LangGraph for intelligent financial analysis. The project features a Next.js frontend with an interactive chat interface and a FastAPI backend powered by Google's Gemini AI for real-time stock analysis and investment recommendations. 4 | 5 | ## 🚀 Features 6 | 7 | - **🤖 AI-Powered Stock Analysis**: Intelligent stock analysis using Google Gemini AI 8 | - **📊 Interactive Charts**: Real-time portfolio performance visualization with Recharts 9 | - **💬 Chat Interface**: Natural language conversation with AI investment advisor 10 | - **📈 Portfolio Management**: Track investments, allocations, and performance metrics 11 | - **🎯 Investment Insights**: Bull and bear market insights for informed decision-making 12 | - **🔄 Real-time Updates**: Live portfolio tracking and state management 13 | - **📱 Responsive Design**: Modern UI built with Next.js 15 and Tailwind CSS 14 | - **🔧 Tool Integration**: Yahoo Finance API integration for real-time stock data 15 | - **🌐 AG-UI Protocol**: Event-driven communication between frontend and LangGraph agent 16 | - **📡 Real-time Streaming**: Server-sent events for live agent interactions 17 | 18 | ## 🛠 Tech Stack 19 | 20 | ### Frontend 21 | 22 | - **Framework**: Next.js 15 with React 19 23 | - **Styling**: Tailwind CSS 4 24 | - **Charts**: Recharts for data visualization 25 | - **AI Integration**: CopilotKit React components 26 | - **Icons**: Lucide React 27 | - **Language**: TypeScript 28 | 29 | ### Backend 30 | 31 | - **Framework**: FastAPI with Python 3.12 32 | - **AI Engine**: LangChain with Google Gemini 33 | - **Workflow**: LangGraph for agent orchestration 34 | - **AG-UI Protocol**: Event-driven agent communication framework 35 | - **Data**: Yahoo Finance (yfinance) for stock data 36 | - **Search**: Tavily for web research 37 | - **Data Processing**: Pandas for financial analysis 38 | - **Environment**: Poetry for dependency management 39 | 40 | ## 📁 Project Structure 41 | 42 | ``` 43 | open-ag-ui-demo-langgraph/ 44 | ├── frontend/ # Next.js 15 React application 45 | │ ├── src/ 46 | │ │ ├── app/ 47 | │ │ │ ├── components/ # UI components 48 | │ │ │ │ ├── chart-components/ # Chart visualizations 49 | │ │ │ │ ├── cash-panel.tsx # Cash management UI 50 | │ │ │ │ ├── generative-canvas.tsx # AI canvas 51 | │ │ │ │ └── prompt-panel.tsx # Chat interface 52 | │ │ │ ├── api/ 53 | │ │ │ │ └── copilotkit/ # CopilotKit API routes 54 | │ │ │ └── page.tsx # Main application page 55 | │ │ └── utils/ 56 | │ │ └── prompts.ts # Frontend prompt templates 57 | │ ├── package.json # Node.js dependencies 58 | │ └── next.config.ts # Next.js configuration 59 | └── agent/ # FastAPI backend agent 60 | ├── main.py # FastAPI application entry point 61 | ├── stock_analysis.py # LangGraph agent implementation 62 | ├── prompts.py # AI prompt templates 63 | ├── pyproject.toml # Python dependencies 64 | └── README.md # Backend documentation 65 | ``` 66 | 67 | --- 68 | 69 | ## 🏗 AG-UI Architecture 70 | 71 | This project demonstrates the **Agent User Interaction Protocol (AG-UI)**, which provides a standardized, event-driven communication framework between frontend applications and AI agents. The AG-UI protocol enables real-time, streaming interactions with LangGraph-powered agents. 72 | 73 | ### Core AG-UI Components 74 | 75 | #### Event-Driven Communication 76 | 77 | The AG-UI protocol uses a streaming event-based architecture where all communication between the frontend and LangGraph agent happens through typed events: 78 | 79 | - **Lifecycle Events**: `RunStarted`, `RunFinished`, `StepStarted`, `StepFinished` 80 | - **Text Message Events**: `TextMessageStart`, `TextMessageContent`, `TextMessageEnd` 81 | - **Tool Call Events**: `ToolCallStart`, `ToolCallArgs`, `ToolCallEnd` 82 | - **State Management Events**: `StateSnapshot`, `StateDelta`, `MessagesSnapshot` 83 | 84 | #### LangGraph Agent Integration 85 | 86 | The backend implements a LangGraph workflow that communicates through AG-UI events: 87 | 88 | ```python 89 | # Agent state flows through LangGraph nodes 90 | class AgentState(CopilotKitState): 91 | tools: list 92 | messages: list 93 | be_stock_data: Any 94 | available_cash: int 95 | # ... other state properties 96 | 97 | # LangGraph workflow with AG-UI event emission 98 | agent_graph = StateGraph(AgentState) 99 | agent_graph.add_node("stock_analysis", stock_analysis_node) 100 | agent_graph.add_node("portfolio_optimization", portfolio_node) 101 | ``` 102 | 103 | #### Real-time Streaming 104 | 105 | - **Server-Sent Events (SSE)**: Enables real-time streaming of agent responses 106 | - **Progressive Content Delivery**: Text and data stream incrementally as generated 107 | - **Live State Updates**: Portfolio data and charts update in real-time 108 | - **Tool Execution Visibility**: Users see AI actions as they happen 109 | 110 | #### State Synchronization 111 | 112 | AG-UI provides efficient state management between frontend and backend: 113 | 114 | - **State Snapshots**: Complete state synchronization at key points 115 | - **State Deltas**: Incremental updates using JSON Patch (RFC 6902) 116 | - **Message History**: Conversation state maintained across interactions 117 | - **Tool Results**: Bidirectional data flow for tool executions 118 | 119 | ### Implementation Details 120 | 121 | #### Backend (FastAPI + LangGraph) 122 | 123 | ```python 124 | @app.post("/run_agent") 125 | async def run_agent_endpoint(input_data: RunAgentInput): 126 | """ 127 | AG-UI compatible endpoint that: 128 | 1. Receives RunAgentInput with tools and context 129 | 2. Executes LangGraph workflow 130 | 3. Streams AG-UI events via SSE 131 | """ 132 | return StreamingResponse( 133 | run_agent_stream(input_data), 134 | media_type="text/event-stream" 135 | ) 136 | ``` 137 | 138 | #### Frontend (CopilotKit + AG-UI) 139 | 140 | ```typescript 141 | // CopilotKit integration with AG-UI events 142 | const { agent } = useCoAgent({ 143 | name: "stock_analysis_agent", 144 | initialState: portfolioState, 145 | }); 146 | 147 | // Real-time state updates via AG-UI events 148 | useCoAgentStateRender({ 149 | name: "stock_analysis_agent", 150 | render: ({ state }) => , 151 | }); 152 | ``` 153 | 154 | ### Benefits of AG-UI Integration 155 | 156 | 1. **Standardized Protocol**: Consistent communication interface regardless of AI backend 157 | 2. **Real-time Interactions**: Streaming responses create responsive user experiences 158 | 3. **Tool Integration**: Seamless bidirectional tool execution between AI and frontend 159 | 4. **State Management**: Efficient synchronization of complex application state 160 | 5. **Extensibility**: Easy to add new agent capabilities and frontend features 161 | 6. **Debugging**: Event-driven architecture provides clear visibility into agent operations 162 | 163 | --- 164 | 165 | ## 🚀 Getting Started 166 | 167 | ### Prerequisites 168 | 169 | Before running the application, ensure you have the following installed: 170 | 171 | - **Node.js** (v18 or later) 172 | - **pnpm** (recommended package manager for frontend) 173 | - **Python** (3.12) 174 | - **Poetry** (for Python dependency management) 175 | - **Google Gemini API Key** (for AI functionality) 176 | 177 | ### 1. Environment Configuration 178 | 179 | Create a `.env` file in each relevant directory with the required API keys. 180 | 181 | #### Backend (`agent/.env`): 182 | 183 | ```env 184 | GOOGLE_API_KEY=<> 185 | # Optional: Add other API keys for enhanced functionality 186 | # TAVILY_API_KEY=<> 187 | ``` 188 | 189 | #### Frontend (`frontend/.env`): 190 | 191 | ```env 192 | GOOGLE_API_KEY=<> 193 | ``` 194 | 195 | ### 2. Start the Backend Agent 196 | 197 | Navigate to the agent directory and install dependencies: 198 | 199 | ```bash 200 | cd agent 201 | poetry install 202 | poetry run python main.py 203 | ``` 204 | 205 | The backend will start on `http://localhost:8000` with the following endpoints: 206 | 207 | - `/run_agent` - Main agent execution endpoint 208 | - `/docs` - FastAPI interactive documentation 209 | 210 | ### 3. Start the Frontend 211 | 212 | In a new terminal, navigate to the frontend directory: 213 | 214 | ```bash 215 | cd frontend 216 | pnpm install 217 | pnpm run dev 218 | ``` 219 | 220 | The frontend will be available at [http://localhost:3000](http://localhost:3000). 221 | 222 | ### 4. Using the Application 223 | 224 | 1. **Open your browser** to `http://localhost:3000` 225 | 2. **Set your investment budget** using the cash panel 226 | 3. **Start chatting** with the AI agent about stocks and investments 227 | 4. **View real-time charts** and portfolio performance 228 | 5. **Get AI insights** on bull and bear market conditions 229 | 230 | --- 231 | 232 | ## 📊 Core Components 233 | 234 | ### Frontend Components 235 | 236 | - **`GenerativeCanvas`** - Main AI chat interface 237 | - **`CashPanel`** - Investment budget management 238 | - **`ComponentTree`** - UI component hierarchy viewer 239 | - **`PromptPanel`** - Chat input and suggestions 240 | - **`BarChart`** - Portfolio allocation visualization 241 | - **`LineChart`** - Performance tracking over time 242 | - **`AllocationTable`** - Detailed portfolio breakdown 243 | - **`ToolLogs`** - AI agent action logging 244 | 245 | ### Backend Agent Features 246 | 247 | - **Stock Data Retrieval** - Yahoo Finance integration 248 | - **AI Analysis** - Google Gemini-powered insights 249 | - **Portfolio Optimization** - Investment allocation recommendations 250 | - **Market Research** - Web search capabilities via Tavily 251 | - **State Management** - LangGraph workflow orchestration 252 | 253 | --- 254 | 255 | ## 🔧 API Endpoints 256 | 257 | ### Backend (FastAPI) 258 | 259 | - `POST /run_agent` - Execute the stock analysis agent 260 | - `GET /docs` - Interactive API documentation 261 | - `GET /redoc` - Alternative API documentation 262 | 263 | ### Frontend (Next.js API Routes) 264 | 265 | - `POST /api/copilotkit` - CopilotKit integration endpoint 266 | 267 | --- 268 | 269 | ## 🎯 Usage Examples 270 | 271 | ### Stock Analysis Query 272 | 273 | ``` 274 | "Analyze AAPL stock and suggest whether I should invest $10,000" 275 | ``` 276 | 277 | ### Portfolio Creation 278 | 279 | ``` 280 | "Create a diversified portfolio for $50,000 with tech and healthcare stocks" 281 | ``` 282 | 283 | ### Market Insights 284 | 285 | ``` 286 | "What are the current market trends and risks I should be aware of?" 287 | ``` 288 | 289 | --- 290 | 291 | ## 🔑 Environment Variables 292 | 293 | | Variable | Description | Required | 294 | | ---------------- | ------------------------------------------ | -------- | 295 | | `GOOGLE_API_KEY` | Google Gemini API key for AI functionality | Yes | 296 | | `TAVILY_API_KEY` | Tavily API key for web search (optional) | No | 297 | 298 | --- 299 | 300 | ## 🛠 Development 301 | 302 | ### Frontend Development 303 | 304 | ```bash 305 | cd frontend 306 | pnpm dev # Start development server 307 | pnpm build # Build for production 308 | pnpm start # Start production server 309 | pnpm lint # Run ESLint 310 | ``` 311 | 312 | ### Backend Development 313 | 314 | ```bash 315 | cd agent 316 | poetry install # Install dependencies 317 | poetry run python main.py # Start development server 318 | poetry run pytest # Run tests (if available) 319 | ``` 320 | 321 | --- 322 | 323 | ## 🚀 Deployment 324 | 325 | ### Frontend (Vercel) 326 | 327 | The frontend is optimized for Vercel deployment: 328 | 329 | 1. Connect your GitHub repository to Vercel 330 | 2. Set environment variables in Vercel dashboard 331 | 3. Deploy automatically on push to main branch 332 | 333 | ### Backend (Self-hosted) 334 | 335 | For production deployment: 336 | 337 | 1. Use a production ASGI server like Gunicorn with Uvicorn workers 338 | 2. Set up proper environment variable management 339 | 3. Configure CORS settings for your frontend domain 340 | 341 | --- 342 | 343 | ## 🔍 Troubleshooting 344 | 345 | ### Common Issues 346 | 347 | 1. **API Key Errors**: Ensure your Google Gemini API key is valid and has sufficient quota 348 | 2. **Backend Connection**: Verify the backend is running on port 8000 before starting frontend 349 | 3. **Dependencies**: Run `poetry install` and `pnpm install` to ensure all dependencies are installed 350 | 4. **Python Version**: Ensure you're using Python 3.12 as specified in pyproject.toml 351 | 352 | ### Debug Mode 353 | 354 | Enable debug logging by setting environment variables: 355 | 356 | ```env 357 | DEBUG=true 358 | LOG_LEVEL=debug 359 | ``` 360 | 361 | --- 362 | 363 | ## 📝 Notes 364 | 365 | - **Backend Dependency**: Ensure the backend agent is running before using the frontend 366 | - **API Keys**: Update environment variables as needed for your deployment 367 | - **Performance**: The application fetches real-time stock data, so response times may vary 368 | - **Rate Limits**: Be mindful of API rate limits for Google Gemini and Yahoo Finance 369 | - **Data Accuracy**: Stock data is for demonstration purposes; consult financial advisors for real investments 370 | 371 | --- 372 | 373 | ## 🔗 Links 374 | 375 | - **Live Demo**: [https://open-ag-ui-demo.vercel.app/](https://open-ag-ui-demo.vercel.app/) 376 | - **AG-UI Documentation**: [https://docs.ag-ui.com/](https://docs.ag-ui.com/) 377 | - **CopilotKit**: [https://copilotkit.ai/](https://copilotkit.ai/) 378 | - **LangGraph**: [https://langchain-ai.github.io/langgraph/](https://langchain-ai.github.io/langgraph/) 379 | - **Google Gemini**: [https://ai.google.dev/](https://ai.google.dev/) 380 | 381 | --- 382 | 383 | ## 📄 License 384 | 385 | This project is open source and available under the [MIT License](LICENSE). 386 | 387 | --- 388 | 389 | ## 🤝 Contributing 390 | 391 | Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change. 392 | 393 | --- 394 | 395 | ## 📞 Support 396 | 397 | If you encounter any issues or have questions: 398 | 399 | 1. Check the [troubleshooting section](#-troubleshooting) 400 | 2. Open an issue in the GitHub repository 401 | 3. Contact the CopilotKit team for framework-specific questions 402 | 403 | --- 404 | 405 | **Built with ❤️ using CopilotKit, LangGraph, and modern web technologies** 406 | -------------------------------------------------------------------------------- /frontend/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | import { PromptPanel } from "./components/prompt-panel"; 5 | import { GenerativeCanvas } from "./components/generative-canvas"; 6 | import { ComponentTree } from "./components/component-tree"; 7 | import { CashPanel } from "./components/cash-panel"; 8 | import { 9 | useCoAgent, 10 | useCoAgentStateRender, 11 | useCopilotAction, 12 | useCopilotReadable, 13 | } from "@copilotkit/react-core"; 14 | import { BarChartComponent } from "@/app/components/chart-components/bar-chart"; 15 | import { LineChartComponent } from "@/app/components/chart-components/line-chart"; 16 | import { AllocationTableComponent } from "@/app/components/chart-components/allocation-table"; 17 | import { useCopilotChatSuggestions } from "@copilotkit/react-ui"; 18 | import { INVESTMENT_SUGGESTION_PROMPT } from "@/utils/prompts"; 19 | import { ToolLogs } from "./components/tool-logs"; 20 | 21 | export interface PortfolioState { 22 | id: string; 23 | trigger: string; 24 | investmentAmount?: number; 25 | currentPortfolioValue?: number; 26 | performanceData: Array<{ 27 | date: string; 28 | portfolio: number; 29 | spy: number; 30 | }>; 31 | allocations: Array<{ 32 | ticker: string; 33 | allocation: number; 34 | currentValue: number; 35 | totalReturn: number; 36 | }>; 37 | returnsData: Array<{ 38 | ticker: string; 39 | return: number; 40 | }>; 41 | bullInsights: Array<{ 42 | title: string; 43 | description: string; 44 | emoji: string; 45 | }>; 46 | bearInsights: Array<{ 47 | title: string; 48 | description: string; 49 | emoji: string; 50 | }>; 51 | totalReturns: number; 52 | } 53 | 54 | export interface SandBoxPortfolioState { 55 | performanceData: Array<{ 56 | date: string; 57 | portfolio: number; 58 | spy: number; 59 | }>; 60 | } 61 | export interface InvestmentPortfolio { 62 | ticker: string; 63 | amount: number; 64 | } 65 | 66 | export default function OpenStocksCanvas() { 67 | const [currentState, setCurrentState] = useState({ 68 | id: "", 69 | trigger: "", 70 | performanceData: [], 71 | allocations: [], 72 | returnsData: [], 73 | bullInsights: [], 74 | bearInsights: [], 75 | currentPortfolioValue: 0, 76 | totalReturns: 0, 77 | }); 78 | const [sandBoxPortfolio, setSandBoxPortfolio] = useState< 79 | SandBoxPortfolioState[] 80 | >([]); 81 | const [selectedStock, setSelectedStock] = useState(null); 82 | const [showComponentTree, setShowComponentTree] = useState(false); 83 | const [totalCash, setTotalCash] = useState(1000000); 84 | const [investedAmount, setInvestedAmount] = useState(0); 85 | 86 | const { state, setState } = useCoAgent({ 87 | name: "stockAgent", 88 | initialState: { 89 | available_cash: totalCash, 90 | investment_summary: {} as any, 91 | investment_portfolio: [] as InvestmentPortfolio[], 92 | }, 93 | }); 94 | 95 | useCoAgentStateRender({ 96 | name: "stockAgent", 97 | render: ({ state }) => , 98 | }); 99 | 100 | useCopilotAction({ 101 | name: "render_standard_charts_and_table", 102 | description: 103 | "This is an action to render a standard chart and table. The chart can be a bar chart or a line chart. The table can be a table of data.", 104 | renderAndWaitForResponse: ({ args, respond, status }) => { 105 | useEffect(() => { 106 | console.log(args, "argsargsargsargsargsaaa"); 107 | }, [args]); 108 | return ( 109 | <> 110 | {args?.investment_summary?.percent_allocation_per_stock && 111 | args?.investment_summary?.percent_return_per_stock && 112 | args?.investment_summary?.performanceData && ( 113 | <> 114 |
115 | 119 | ({ 123 | ticker, 124 | return: return1 as number, 125 | }))} 126 | size="small" 127 | /> 128 | ({ 132 | ticker, 133 | allocation: allocation as number, 134 | currentValue: 135 | args?.investment_summary.final_prices[ticker] * 136 | args?.investment_summary.holdings[ticker], 137 | totalReturn: 138 | args?.investment_summary.percent_return_per_stock[ 139 | ticker 140 | ], 141 | }))} 142 | size="small" 143 | /> 144 |
145 | 146 | 204 | 217 | 218 | )} 219 | 220 | ); 221 | }, 222 | }); 223 | 224 | useCopilotAction({ 225 | name: "render_custom_charts", 226 | renderAndWaitForResponse: ({ args, respond, status }) => { 227 | return ( 228 | <> 229 | 233 | 259 | 272 | 273 | ); 274 | }, 275 | }); 276 | 277 | useCopilotReadable({ 278 | description: "This is the current state of the portfolio", 279 | value: JSON.stringify(state.investment_portfolio), 280 | }); 281 | 282 | useCopilotChatSuggestions( 283 | { 284 | available: selectedStock ? "disabled" : "enabled", 285 | instructions: INVESTMENT_SUGGESTION_PROMPT, 286 | }, 287 | [selectedStock] 288 | ); 289 | 290 | // const toggleComponentTree = () => { 291 | // setShowComponentTree(!showComponentTree) 292 | // } 293 | 294 | // const availableCash = totalCash - investedAmount 295 | // const currentPortfolioValue = currentState.currentPortfolioValue || investedAmount 296 | 297 | useEffect(() => { 298 | getBenchmarkData(); 299 | }, []); 300 | 301 | function getBenchmarkData() { 302 | let result: PortfolioState = { 303 | id: "aapl-nvda", 304 | trigger: "apple nvidia", 305 | performanceData: [ 306 | { date: "Jan 2023", portfolio: 10000, spy: 10000 }, 307 | { date: "Mar 2023", portfolio: 10200, spy: 10200 }, 308 | { date: "Jun 2023", portfolio: 11000, spy: 11000 }, 309 | { date: "Sep 2023", portfolio: 10800, spy: 10800 }, 310 | { date: "Dec 2023", portfolio: 11500, spy: 11500 }, 311 | { date: "Mar 2024", portfolio: 12200, spy: 12200 }, 312 | { date: "Jun 2024", portfolio: 12800, spy: 12800 }, 313 | { date: "Sep 2024", portfolio: 13100, spy: 13100 }, 314 | { date: "Dec 2024", portfolio: 13600, spy: 13600 }, 315 | ], 316 | allocations: [], 317 | returnsData: [], 318 | bullInsights: [], 319 | bearInsights: [], 320 | totalReturns: 0, 321 | currentPortfolioValue: totalCash, 322 | }; 323 | setCurrentState(result); 324 | } 325 | 326 | return ( 327 |
328 | {/* Left Panel - Prompt Input */} 329 |
330 | 331 |
332 | 333 | {/* Center Panel - Generative Canvas */} 334 |
335 | {/* Top Bar with Cash Info */} 336 |
337 | 346 |
347 | 348 | {/*
349 | 355 |
*/} 356 | 357 |
358 | 364 |
365 |
366 | 367 | {/* Right Panel - Component Tree (Optional) */} 368 | {showComponentTree && ( 369 |
370 | 371 |
372 | )} 373 |
374 | ); 375 | } 376 | -------------------------------------------------------------------------------- /agent/README.md: -------------------------------------------------------------------------------- 1 | # Stock Analysis Agent with AG-UI Protocol 2 | 3 | A FastAPI-based backend agent demonstrating the AG-UI (Agent User Interaction) protocol implementation with LangGraph workflow orchestration for stock analysis and investment simulation. 4 | 5 | ## Overview 6 | 7 | This agent demonstrates how to build AI agents using the AG-UI protocol, featuring: 8 | 9 | - **Event-driven communication** through AG-UI's streaming protocol 10 | - **LangGraph workflow orchestration** with state management 11 | - **Tool-based interactions** with real-time progress tracking 12 | - **Structured state synchronization** between agent and frontend 13 | - **Multi-step agent workflows** with intermediate feedback 14 | 15 | ### Core Capabilities 16 | 17 | - Stock data extraction and analysis using Yahoo Finance 18 | - Investment simulation with portfolio tracking 19 | - AI-powered bull/bear insights generation using Google Gemini 20 | - Performance comparison with SPY benchmark 21 | - Real-time streaming updates via AG-UI events 22 | 23 | ## AG-UI Protocol Features 24 | 25 | This implementation showcases key AG-UI protocol capabilities: 26 | 27 | - **Event Streaming**: Real-time communication via Server-Sent Events (SSE) 28 | - **State Management**: Synchronized state between agent and frontend using `STATE_SNAPSHOT` and `STATE_DELTA` events 29 | - **Tool Integration**: Frontend-defined tools executed by the agent with progress tracking 30 | - **Message History**: Persistent conversation context across interactions 31 | - **Lifecycle Events**: Clear workflow progression indicators (`RUN_STARTED`, `RUN_FINISHED`) 32 | - **Error Handling**: Robust error propagation and recovery mechanisms 33 | 34 | ### AG-UI Event Types Used 35 | 36 | - **Lifecycle Events**: `RUN_STARTED`, `RUN_FINISHED` for agent execution boundaries 37 | - **Text Message Events**: `TEXT_MESSAGE_START`, `TEXT_MESSAGE_CONTENT`, `TEXT_MESSAGE_END` for streaming responses 38 | - **Tool Call Events**: `TOOL_CALL_START`, `TOOL_CALL_ARGS`, `TOOL_CALL_END` for tool execution tracking 39 | - **State Events**: `STATE_SNAPSHOT`, `STATE_DELTA` for real-time state synchronization 40 | 41 | ## Architecture 42 | 43 | This agent demonstrates the AG-UI protocol architecture with: 44 | 45 | ### AG-UI Protocol Layer 46 | 47 | - **FastAPI Server**: Implements AG-UI HTTP endpoint (`/langgraph-agent`) 48 | - **Event Encoder**: Converts internal events to AG-UI SSE format 49 | - **State Synchronization**: Real-time state updates between agent and frontend 50 | - **Tool Protocol**: Standardized tool call/response handling 51 | 52 | ### LangGraph Integration 53 | 54 | - **Workflow Orchestration**: Multi-node agent workflow with state transitions 55 | - **State Management**: Persistent state across workflow nodes using `AgentState` 56 | - **Node-based Processing**: Modular agent logic with clear separation of concerns 57 | - **Command Routing**: Dynamic workflow navigation based on processing results 58 | 59 | ### Technology Stack 60 | 61 | - **AG-UI Protocol**: Event-driven agent communication standard 62 | - **LangGraph**: Workflow orchestration and state management 63 | - **FastAPI**: Web server with SSE streaming support 64 | - **Google Gemini 2.5 Pro**: Large language model for AI capabilities 65 | - **Yahoo Finance API**: Real-time and historical stock market data 66 | - **CopilotKit State**: Extended state management for UI integration 67 | 68 | ## Prerequisites 69 | 70 | - Python 3.12 71 | - Poetry (for dependency management) 72 | - Google API Key for Gemini access 73 | 74 | ## Setup 75 | 76 | ### 1. Install Dependencies 77 | 78 | ```bash 79 | poetry install 80 | ``` 81 | 82 | ### 2. Environment Configuration 83 | 84 | Create a `.env` file in the agent directory: 85 | 86 | ```env 87 | # Required: Google API Key for Gemini 2.5 Pro 88 | GOOGLE_API_KEY=your-google-api-key-here 89 | 90 | # Optional: Server port (default: 8000) 91 | PORT=8000 92 | ``` 93 | 94 | Get your Google API key from [Google AI Studio](https://makersuite.google.com/app/apikey). 95 | 96 | ### 3. Run the Agent 97 | 98 | ```bash 99 | poetry run python main.py 100 | ``` 101 | 102 | The agent will start on `http://localhost:8000`. 103 | 104 | ## AG-UI Protocol Implementation 105 | 106 | ### HTTP Endpoint 107 | 108 | The agent implements the AG-UI HTTP protocol via: 109 | 110 | **POST `/langgraph-agent`** 111 | 112 | Accepts `RunAgentInput` and returns a stream of AG-UI events. 113 | 114 | **Request Body (RunAgentInput):** 115 | 116 | ```json 117 | { 118 | "thread_id": "conversation-123", 119 | "run_id": "run-456", 120 | "messages": [ 121 | { 122 | "id": "msg-1", 123 | "role": "user", 124 | "content": "Analyze AAPL and GOOGL with $10000 investment each" 125 | } 126 | ], 127 | "tools": [ 128 | { 129 | "name": "extract_relevant_data_from_user_prompt", 130 | "description": "Gets ticker symbols, amounts, and investment parameters" 131 | } 132 | ], 133 | "state": { 134 | "available_cash": 50000, 135 | "investment_portfolio": [], 136 | "investment_summary": {} 137 | } 138 | } 139 | ``` 140 | 141 | **Response:** Server-Sent Events stream containing AG-UI protocol events: 142 | 143 | ``` 144 | data: {"type": "RUN_STARTED", "thread_id": "conversation-123", "run_id": "run-456"} 145 | 146 | data: {"type": "STATE_SNAPSHOT", "snapshot": {"available_cash": 50000, "tool_logs": []}} 147 | 148 | data: {"type": "TOOL_CALL_START", "tool_call_id": "call-123", "toolCallName": "extract_relevant_data_from_user_prompt"} 149 | 150 | data: {"type": "STATE_DELTA", "delta": [{"op": "add", "path": "/tool_logs/-", "value": {"message": "Extracting investment parameters", "status": "processing"}}]} 151 | 152 | data: {"type": "RUN_FINISHED", "thread_id": "conversation-123", "run_id": "run-456"} 153 | ``` 154 | 155 | ## LangGraph Workflow Implementation 156 | 157 | The agent uses LangGraph to orchestrate a multi-step workflow with AG-UI event streaming: 158 | 159 | ### Workflow Nodes 160 | 161 | 1. **Chat Node** (`chat_node`) 162 | 163 | - Extracts investment parameters using LLM tool calls 164 | - Emits `TOOL_CALL_*` events for parameter extraction 165 | - Updates conversation state with structured data 166 | 167 | 2. **Simulation Node** (`simulation_node`) 168 | 169 | - Fetches historical stock data from Yahoo Finance 170 | - Emits `STATE_DELTA` events for portfolio updates 171 | - Validates and adjusts investment dates 172 | 173 | 3. **Cash Allocation Node** (`cash_allocation_node`) 174 | 175 | - Simulates investment strategies (single-shot vs DCA) 176 | - Calculates returns and portfolio performance 177 | - Compares against SPY benchmark 178 | 179 | 4. **Insights Node** (`insights_node`) 180 | 181 | - Generates AI-powered bull/bear analysis 182 | - Uses structured tool calls for insight generation 183 | - Streams final recommendations 184 | 185 | 5. **End Node** (`end_node`) 186 | - Workflow termination marker 187 | - Triggers final state cleanup 188 | 189 | ### State Flow and AG-UI Integration 190 | 191 | ```python 192 | class AgentState(CopilotKitState): 193 | """AG-UI compatible state that flows through LangGraph nodes""" 194 | tools: list # Available tools for the agent 195 | messages: list # Conversation history (AG-UI format) 196 | be_stock_data: Any # Yahoo Finance data 197 | be_arguments: dict # Extracted investment parameters 198 | available_cash: int # Current cash balance 199 | investment_summary: dict # Performance metrics 200 | investment_portfolio: list # Holdings 201 | tool_logs: list # Real-time progress tracking 202 | ``` 203 | 204 | Each node in the workflow: 205 | 206 | - Receives the current `AgentState` 207 | - Emits AG-UI events via `emit_event()` callback 208 | - Updates state for the next node 209 | - Returns `Command` objects for workflow routing 210 | 211 | ## Key Components 212 | 213 | ### AG-UI State Management 214 | 215 | The `AgentState` class extends `CopilotKitState` and provides: 216 | 217 | - **Message Synchronization**: Maintains conversation history in AG-UI format 218 | - **Real-time Updates**: Emits `STATE_DELTA` events for incremental changes 219 | - **Tool Logging**: Tracks tool execution progress for UI feedback 220 | - **Portfolio Tracking**: Manages investment data with live updates 221 | 222 | ```python 223 | # Example state delta emission 224 | config.get("configurable").get("emit_event")( 225 | StateDeltaEvent( 226 | type=EventType.STATE_DELTA, 227 | delta=[{ 228 | "op": "replace", 229 | "path": "/available_cash", 230 | "value": new_cash_amount 231 | }] 232 | ) 233 | ) 234 | ``` 235 | 236 | ### AG-UI Tool Integration 237 | 238 | Tools are defined with JSON schema and executed through the AG-UI protocol: 239 | 240 | - **Frontend-Defined Tools**: Tools are passed from frontend to agent 241 | - **Structured Execution**: Tool calls follow `START` → `ARGS` → `END` event sequence 242 | - **Progress Tracking**: Each tool execution updates `tool_logs` with status 243 | - **Type Safety**: Full TypeScript/Python type checking for tool parameters 244 | 245 | #### Tool: `extract_relevant_data_from_user_prompt` 246 | 247 | Extracts structured investment parameters from natural language: 248 | 249 | - Ticker symbols array (e.g., `['AAPL', 'GOOGL']`) 250 | - Investment amounts per stock 251 | - Investment date and interval strategy 252 | - Portfolio vs sandbox selection 253 | 254 | #### Tool: `generate_insights` 255 | 256 | Creates bull/bear case analysis with structured output: 257 | 258 | - Positive insights with titles, descriptions, and emojis 259 | - Negative insights with risk assessments 260 | - AI-generated investment thesis for each stock 261 | 262 | ### Event-Driven Data Processing 263 | 264 | The agent combines LangGraph workflow orchestration with AG-UI event streaming: 265 | 266 | - **Async Processing**: Each node runs asynchronously with real-time updates 267 | - **Progress Feedback**: Tool logs provide immediate user feedback 268 | - **State Synchronization**: Frontend state stays synchronized via delta events 269 | - **Error Handling**: Graceful error propagation through AG-UI events 270 | 271 | #### Data Flow Example: 272 | 273 | 1. User submits investment query 274 | 2. `chat_node` emits `TOOL_CALL_START` event 275 | 3. LLM extracts parameters, emits `STATE_DELTA` with results 276 | 4. `simulation_node` fetches Yahoo Finance data 277 | 5. Portfolio state updated via `STATE_DELTA` events 278 | 6. Final insights streamed as `TEXT_MESSAGE_*` events 279 | 280 | ## AG-UI Protocol Configuration 281 | 282 | ### Event Encoder Setup 283 | 284 | ```python 285 | from ag_ui.encoder import EventEncoder 286 | 287 | encoder = EventEncoder() 288 | yield encoder.encode( 289 | StateSnapshotEvent( 290 | type=EventType.STATE_SNAPSHOT, 291 | snapshot=initial_state 292 | ) 293 | ) 294 | ``` 295 | 296 | ### Runtime Configuration 297 | 298 | The agent accepts configuration for AG-UI integration: 299 | 300 | ```python 301 | config = { 302 | "configurable": { 303 | "emit_event": event_callback, # AG-UI event emission 304 | "message_id": unique_id # Message tracking 305 | } 306 | } 307 | ``` 308 | 309 | ### Investment Parameters 310 | 311 | - **Ticker Symbols**: Stock symbols to analyze (e.g., AAPL, GOOGL) 312 | - **Investment Amount**: Dollar amounts to invest per stock 313 | - **Investment Date**: Starting date for analysis 314 | - **Investment Interval**: `single_shot` or periodic intervals 315 | - **Portfolio Type**: Current portfolio or sandbox 316 | 317 | ### Data Periods 318 | 319 | - Automatically adjusts historical data periods based on investment date 320 | - Limits lookback to 4 years maximum for performance 321 | - Uses quarterly intervals for analysis 322 | 323 | ## Dependencies 324 | 325 | Key dependencies for AG-UI and LangGraph integration: 326 | 327 | - **ag-ui-core**: Core AG-UI protocol events and types 328 | - **ag-ui-encoder**: Event encoding for SSE streaming 329 | - **copilotkit**: Extended state management and CopilotKit integration 330 | - **langgraph**: Workflow orchestration with state transitions 331 | - **langchain**: LLM integration and tool calling 332 | - **langchain-gemini**: Google Gemini model integration 333 | - **fastapi**: HTTP server with SSE streaming capabilities 334 | - **yfinance**: Yahoo Finance API for market data 335 | - **pandas**: Data manipulation and analysis 336 | - **uvicorn**: ASGI server for production deployment 337 | 338 | ### AG-UI Specific Imports 339 | 340 | ```python 341 | from ag_ui.core import ( 342 | RunAgentInput, StateSnapshotEvent, EventType, 343 | RunStartedEvent, RunFinishedEvent, 344 | TextMessageStartEvent, TextMessageContentEvent, 345 | ToolCallStartEvent, StateDeltaEvent 346 | ) 347 | from ag_ui.encoder import EventEncoder 348 | ``` 349 | 350 | ## Error Handling 351 | 352 | The agent includes robust error handling for: 353 | 354 | - Missing or invalid stock data 355 | - Insufficient funds scenarios 356 | - API rate limiting 357 | - Network connectivity issues 358 | 359 | ## Development 360 | 361 | ### File Structure 362 | 363 | ``` 364 | agent/ 365 | ├── main.py # FastAPI server with AG-UI endpoint 366 | ├── stock_analysis.py # LangGraph workflow and AG-UI integration 367 | ├── prompts.py # System and insight prompts for LLM 368 | ├── pyproject.toml # Dependencies including AG-UI packages 369 | ├── poetry.lock # Locked dependencies 370 | ├── .env # Environment variables 371 | └── README.md # This documentation 372 | ``` 373 | 374 | ### Adding New Features 375 | 376 | #### Adding LangGraph Nodes 377 | 378 | 1. Define new async node function in `stock_analysis.py` 379 | 2. Add AG-UI event emissions for progress tracking 380 | 3. Update `AgentState` schema if needed 381 | 4. Connect node to workflow graph with routing logic 382 | 383 | #### Adding AG-UI Events 384 | 385 | 1. Import event types from `ag_ui.core` 386 | 2. Emit events via `config.get("configurable").get("emit_event")()` 387 | 3. Use appropriate event types (`STATE_DELTA`, `TOOL_CALL_*`, etc.) 388 | 4. Test event streaming in frontend integration 389 | 390 | #### Adding Tools 391 | 392 | 1. Define tool schema with JSON parameters 393 | 2. Implement tool logic in workflow nodes 394 | 3. Emit `TOOL_CALL_*` events for execution tracking 395 | 4. Handle tool results and state updates 396 | 397 | ### Testing AG-UI Integration 398 | 399 | Test the protocol implementation: 400 | 401 | ```bash 402 | # Start the agent 403 | poetry run python main.py 404 | 405 | # Test with curl (example POST) 406 | curl -X POST http://localhost:8000/langgraph-agent \ 407 | -H "Content-Type: application/json" \ 408 | -d '{"thread_id": "test", "run_id": "run1", "messages": [...], "tools": [...], "state": {...}}' 409 | ``` 410 | 411 | ## Troubleshooting 412 | 413 | ### AG-UI Protocol Issues 414 | 415 | 1. **Event Streaming Problems**: Check SSE headers and event encoding format 416 | 2. **State Sync Issues**: Verify `STATE_DELTA` events use correct JSON Patch format 417 | 3. **Tool Call Failures**: Ensure tool schemas match frontend definitions 418 | 4. **Message Format Errors**: Validate AG-UI message structure and types 419 | 420 | ### LangGraph Issues 421 | 422 | 1. **Workflow Hangs**: Check node return values and Command routing 423 | 2. **State Corruption**: Verify state mutations are properly handled 424 | 3. **Node Errors**: Add error handling and recovery in each workflow node 425 | 4. **Memory Issues**: Monitor state size and data retention 426 | 427 | ### Common Issues 428 | 429 | 1. **Missing API Key**: Ensure `GOOGLE_API_KEY` is set in `.env` 430 | 2. **Stock Data Issues**: Yahoo Finance may have temporary outages or rate limits 431 | 3. **Event Encoding**: Verify AG-UI event types and encoding format 432 | 4. **State Management**: Check JSON Patch operations in `STATE_DELTA` events 433 | 434 | ### Debugging 435 | 436 | Enable detailed logging for AG-UI and LangGraph: 437 | 438 | ```python 439 | import logging 440 | logging.basicConfig(level=logging.DEBUG) 441 | 442 | # Add debug prints in workflow nodes 443 | print(f"Current state: {state}") 444 | print(f"Emitting event: {event}") 445 | ``` 446 | 447 | ## Contributing 448 | 449 | 1. Fork the repository 450 | 2. Create a feature branch 451 | 3. Implement AG-UI protocol changes or LangGraph workflow improvements 452 | 4. Test with both unit tests and integration tests 453 | 5. Ensure event streaming works correctly with frontend 454 | 6. Submit a pull request with detailed description 455 | 456 | ### Best Practices 457 | 458 | - Follow AG-UI protocol specifications for event types and formats 459 | - Use proper error handling and recovery in LangGraph nodes 460 | - Emit progress events for long-running operations 461 | - Maintain state consistency across workflow transitions 462 | - Document new tools and their AG-UI integration 463 | 464 | ## License 465 | 466 | This project is part of the CopilotKit demo suite demonstrating AG-UI protocol implementation with LangGraph workflow orchestration. 467 | -------------------------------------------------------------------------------- /agent/stock_analysis.py: -------------------------------------------------------------------------------- 1 | # Import necessary libraries for LangChain, LangGraph, and financial analysis 2 | from langchain_core.runnables import RunnableConfig # Configuration for LangChain runnables 3 | from ag_ui.core import StateDeltaEvent, EventType # AG UI event system for state updates 4 | from langchain_core.messages import SystemMessage, AIMessage, ToolMessage, HumanMessage # Message types 5 | from ag_ui.core.types import AssistantMessage, ToolMessage as ToolMessageAGUI # AG UI message types 6 | from langchain_core.tools import tool # Decorator for creating tools 7 | from langgraph.graph import StateGraph, START, END # LangGraph workflow components 8 | from langgraph.types import Command # For controlling workflow flow 9 | import yfinance as yf # Yahoo Finance API for stock data 10 | from copilotkit import CopilotKitState # Base state class from CopilotKit 11 | from langchain.chat_models import init_chat_model # Chat model initialization 12 | from dotenv import load_dotenv # Environment variable loader 13 | import json # JSON handling 14 | import pandas as pd # Data manipulation and analysis 15 | import asyncio # Asynchronous programming 16 | from prompts import system_prompt, insights_prompt # Custom prompts for the agent 17 | from datetime import datetime # Date and time handling 18 | from typing import Any # Type hints 19 | import uuid # Unique identifier generation 20 | 21 | # Load environment variables (API keys, etc.) 22 | load_dotenv() 23 | 24 | 25 | class AgentState(CopilotKitState): 26 | """ 27 | AgentState defines the complete state structure for the stock analysis agent. 28 | This state flows through all nodes in the LangGraph workflow and contains 29 | all the data needed for stock analysis, portfolio management, and UI updates. 30 | """ 31 | 32 | # List of available tools that the agent can call 33 | tools: list 34 | # Conversation history between user and assistant 35 | messages: list 36 | # Stock price data retrieved from Yahoo Finance (pandas DataFrame) 37 | be_stock_data: Any 38 | # Parsed arguments from user input (ticker symbols, amounts, dates, etc.) 39 | be_arguments: dict 40 | # Current available cash in the user's wallet 41 | available_cash: int 42 | # Summary of investment results, returns, and portfolio performance 43 | investment_summary: dict 44 | # List of stocks and amounts in the current portfolio 45 | investment_portfolio: list 46 | # Log of tool executions with status updates for the UI 47 | tool_logs: list 48 | 49 | 50 | def convert_tool_call(tc): 51 | """ 52 | Convert LangChain tool call format to AG UI tool call format. 53 | 54 | Args: 55 | tc: Tool call object from LangChain 56 | 57 | Returns: 58 | dict: Tool call formatted for AG UI with id, type, and function details 59 | """ 60 | return { 61 | "id": tc.get("id"), 62 | "type": "function", 63 | "function": { 64 | "name": tc.get("name"), 65 | "arguments": json.dumps(tc.get("args", {})), 66 | }, 67 | } 68 | 69 | 70 | def convert_tool_call_for_model(tc): 71 | """ 72 | Convert AG UI tool call format back to LangChain model format. 73 | 74 | Args: 75 | tc: Tool call object from AG UI 76 | 77 | Returns: 78 | dict: Tool call formatted for LangChain models with parsed arguments 79 | """ 80 | return { 81 | "id": tc.id, 82 | "name": tc.function.name, 83 | "args": json.loads(tc.function.arguments), 84 | } 85 | 86 | # Tool definition for extracting investment parameters from user input 87 | # This tool is used by the LLM to parse natural language requests into structured data 88 | extract_relevant_data_from_user_prompt = { 89 | "name": "extract_relevant_data_from_user_prompt", 90 | "description": "Gets the data like ticker symbols, amount of dollars to be invested, interval of investment.", 91 | "parameters": { 92 | "type": "object", 93 | "properties": { 94 | # Array of stock ticker symbols (e.g., ['AAPL', 'GOOGL', 'MSFT']) 95 | "ticker_symbols": { 96 | "type": "array", 97 | "items": { 98 | "type": "string", 99 | "description": "A stock ticker symbol, e.g. 'AAPL', 'GOOGL'.", 100 | }, 101 | "description": "A list of stock ticker symbols, e.g. ['AAPL', 'GOOGL'].", 102 | }, 103 | # Date when the investment should be made 104 | "investment_date": { 105 | "type": "string", 106 | "description": "The date of investment, e.g. '2023-01-01'.", 107 | }, 108 | # Amount of money to invest in each stock (parallel to ticker_symbols) 109 | "amount_of_dollars_to_be_invested": { 110 | "type": "array", 111 | "items": { 112 | "type": "number", 113 | "description": "The amount of dollars to be invested, e.g. 10000.", 114 | }, 115 | "description": "The amount of dollars to be invested, e.g. [10000, 20000, 30000].", 116 | }, 117 | # Investment strategy: single purchase vs dollar-cost averaging 118 | "interval_of_investment": { 119 | "type": "string", 120 | "description": "The interval of investment, e.g. '1d', '5d', '1mo', '3mo', '6mo', '1y'1d', '5d', '7d', '1mo', '3mo', '6mo', '1y', '2y', '3y', '4y', '5y'. If the user did not specify the interval, then assume it as 'single_shot'", 121 | }, 122 | # Whether to add to real portfolio or simulate in sandbox 123 | "to_be_added_in_portfolio": { 124 | "type": "boolean", 125 | "description": "If user wants to add it in the current portfolio, then set it to true. If user wants to add it in the sandbox portfolio, then set it to false.", 126 | }, 127 | }, 128 | "required": [ 129 | "ticker_symbols", 130 | "investment_date", 131 | "amount_of_dollars_to_be_invested", 132 | "to_be_added_in_portfolio", 133 | ], 134 | }, 135 | } 136 | 137 | 138 | # Tool definition for generating investment insights (bull and bear cases) 139 | # This tool creates positive and negative analysis for stocks or portfolios 140 | generate_insights = { 141 | "name": "generate_insights", 142 | "description": "Generate positive (bull) and negative (bear) insights for a stock or portfolio.", 143 | "parameters": { 144 | "type": "object", 145 | "properties": { 146 | # Positive investment thesis and opportunities 147 | "bullInsights": { 148 | "type": "array", 149 | "description": "A list of positive insights (bull case) for the stock or portfolio.", 150 | "items": { 151 | "type": "object", 152 | "properties": { 153 | "title": { 154 | "type": "string", 155 | "description": "Short title for the positive insight.", 156 | }, 157 | "description": { 158 | "type": "string", 159 | "description": "Detailed description of the positive insight.", 160 | }, 161 | "emoji": { 162 | "type": "string", 163 | "description": "Emoji representing the positive insight.", 164 | }, 165 | }, 166 | "required": ["title", "description", "emoji"], 167 | }, 168 | }, 169 | # Negative investment thesis and risks 170 | "bearInsights": { 171 | "type": "array", 172 | "description": "A list of negative insights (bear case) for the stock or portfolio.", 173 | "items": { 174 | "type": "object", 175 | "properties": { 176 | "title": { 177 | "type": "string", 178 | "description": "Short title for the negative insight.", 179 | }, 180 | "description": { 181 | "type": "string", 182 | "description": "Detailed description of the negative insight.", 183 | }, 184 | "emoji": { 185 | "type": "string", 186 | "description": "Emoji representing the negative insight.", 187 | }, 188 | }, 189 | "required": ["title", "description", "emoji"], 190 | }, 191 | }, 192 | }, 193 | "required": ["bullInsights", "bearInsights"], 194 | }, 195 | } 196 | 197 | 198 | async def chat_node(state: AgentState, config: RunnableConfig): 199 | """ 200 | First node in the workflow: Analyzes user input and extracts investment parameters. 201 | 202 | This function: 203 | 1. Creates a tool log entry for UI feedback 204 | 2. Initializes the chat model (Gemini 2.5 Pro) 205 | 3. Converts state messages to LangChain format 206 | 4. Uses the LLM to extract structured data from user input 207 | 5. Handles retries if the model doesn't respond properly 208 | 6. Updates the conversation state with the response 209 | 210 | Args: 211 | state: Current agent state containing messages and context 212 | config: Runtime configuration including event emitters 213 | """ 214 | try: 215 | # Step 1: Create and emit a tool log entry for UI feedback 216 | tool_log_id = str(uuid.uuid4()) 217 | state["tool_logs"].append( 218 | {"id": tool_log_id, "message": "Analyzing user query", "status": "processing"} 219 | ) 220 | config.get("configurable").get("emit_event")( 221 | StateDeltaEvent( 222 | type=EventType.STATE_DELTA, 223 | delta=[ 224 | { 225 | "op": "add", 226 | "path": "/tool_logs/-", 227 | "value": { 228 | "message": "Analyzing user query", 229 | "status": "processing", 230 | "id": tool_log_id 231 | }, 232 | } 233 | ], 234 | ) 235 | ) 236 | await asyncio.sleep(0) # Yield control to allow UI updates 237 | 238 | # Step 2: Initialize the chat model (Gemini 2.5 Pro from Google) 239 | model = init_chat_model("gemini-2.5-pro", model_provider="google_genai") 240 | 241 | # Step 3: Convert state messages to LangChain message format 242 | messages = [] 243 | for message in state["messages"]: 244 | match message.role: 245 | case "user": 246 | # Convert user messages to HumanMessage 247 | messages.append(HumanMessage(content=message.content)) 248 | case "system": 249 | # Convert system messages and inject portfolio data 250 | messages.append( 251 | SystemMessage( 252 | content=system_prompt.replace( 253 | "{PORTFOLIO_DATA_PLACEHOLDER}", 254 | json.dumps(state["investment_portfolio"]), 255 | ) 256 | ) 257 | ) 258 | case "assistant" | "ai": 259 | # Convert assistant messages and handle tool calls 260 | tool_calls_converted = [ 261 | convert_tool_call_for_model(tc) 262 | for tc in message.tool_calls or [] 263 | ] 264 | messages.append( 265 | AIMessage( 266 | invalid_tool_calls=[], 267 | tool_calls=tool_calls_converted, 268 | type="ai", 269 | content=message.content or "", 270 | ) 271 | ) 272 | case "tool": 273 | # Convert tool result messages 274 | messages.append( 275 | ToolMessage( 276 | tool_call_id=message.tool_call_id, content=message.content 277 | ) 278 | ) 279 | case _: 280 | raise ValueError(f"Unsupported message role: {message.role}") 281 | 282 | # Step 4: Attempt to get structured response from the model with retries 283 | retry_counter = 0 284 | while True: 285 | # Break after 3 failed attempts to prevent infinite loops 286 | if retry_counter > 3: 287 | print("retry_counter", retry_counter) 288 | break 289 | 290 | # Call the model with the data extraction tool 291 | response = await model.bind_tools( 292 | [extract_relevant_data_from_user_prompt] 293 | ).ainvoke(messages, config=config) 294 | 295 | # Step 5a: Handle successful tool call response 296 | if response.tool_calls: 297 | # Convert tool calls to AG UI format 298 | tool_calls = [convert_tool_call(tc) for tc in response.tool_calls] 299 | a_message = AssistantMessage( 300 | role="assistant", tool_calls=tool_calls, id=response.id 301 | ) 302 | state["messages"].append(a_message) 303 | 304 | # Update tool log status to completed 305 | index = len(state["tool_logs"]) - 1 306 | config.get("configurable").get("emit_event")( 307 | StateDeltaEvent( 308 | type=EventType.STATE_DELTA, 309 | delta=[ 310 | { 311 | "op": "replace", 312 | "path": f"/tool_logs/{index}/status", 313 | "value": "completed" 314 | } 315 | ], 316 | ) 317 | ) 318 | await asyncio.sleep(0) 319 | return # Success - exit the function 320 | 321 | # Step 5b: Handle empty response (retry needed) 322 | elif response.content == "" and response.tool_calls == []: 323 | retry_counter += 1 324 | 325 | # Step 5c: Handle text response (no tool call) 326 | else: 327 | a_message = AssistantMessage( 328 | id=response.id, content=response.content, role="assistant" 329 | ) 330 | state["messages"].append(a_message) 331 | 332 | # Update tool log status to completed 333 | index = len(state["tool_logs"]) - 1 334 | config.get("configurable").get("emit_event")( 335 | StateDeltaEvent( 336 | type=EventType.STATE_DELTA, 337 | delta=[ 338 | { 339 | "op": "replace", 340 | "path": f"/tool_logs/{index}/status", 341 | "value": "completed" 342 | } 343 | ], 344 | ) 345 | ) 346 | await asyncio.sleep(0) 347 | return # Success - exit the function 348 | 349 | # Step 6: Handle case where all retries failed 350 | print("hello") 351 | a_message = AssistantMessage( 352 | id=response.id, content=response.content, role="assistant" 353 | ) 354 | state["messages"].append(a_message) 355 | 356 | except Exception as e: 357 | # Step 7: Handle any exceptions that occur during processing 358 | print(e) 359 | a_message = AssistantMessage(id=response.id, content="", role="assistant") 360 | state["messages"].append(a_message) 361 | return Command( 362 | goto="end", # Skip to end node on error 363 | ) 364 | 365 | # Step 8: Final cleanup - mark tool log as completed 366 | index = len(state["tool_logs"]) - 1 367 | config.get("configurable").get("emit_event")( 368 | StateDeltaEvent( 369 | type=EventType.STATE_DELTA, 370 | delta=[ 371 | { 372 | "op": "replace", 373 | "path": f"/tool_logs/{index}/status", 374 | "value": "completed" 375 | } 376 | ], 377 | ) 378 | ) 379 | await asyncio.sleep(0) 380 | return 381 | 382 | 383 | async def end_node(state: AgentState, config: RunnableConfig): 384 | """ 385 | Terminal node in the workflow: Marks the completion of the agent execution. 386 | 387 | This is a simple placeholder node that signifies the end of the workflow. 388 | No processing is done here - it's just a marker for workflow completion. 389 | """ 390 | print("inside end node") 391 | 392 | 393 | async def simulation_node(state: AgentState, config: RunnableConfig): 394 | """ 395 | Second node in the workflow: Fetches historical stock data for analysis. 396 | 397 | This function: 398 | 1. Creates a tool log entry for UI feedback 399 | 2. Extracts investment parameters from the previous tool call 400 | 3. Updates the investment portfolio in the state 401 | 4. Validates and adjusts the investment date if too far in the past 402 | 5. Downloads historical stock data from Yahoo Finance 403 | 6. Stores the data for use in subsequent nodes 404 | 405 | Args: 406 | state: Current agent state with extracted investment parameters 407 | config: Runtime configuration including event emitters 408 | 409 | Returns: 410 | Command: Directs workflow to the cash_allocation node 411 | """ 412 | print("inside simulation node") 413 | 414 | # Step 1: Create and emit tool log entry for UI feedback 415 | tool_log_id = str(uuid.uuid4()) 416 | state["tool_logs"].append( 417 | {"id": tool_log_id, "message": "Gathering stock data", "status": "processing"} 418 | ) 419 | config.get("configurable").get("emit_event")( 420 | StateDeltaEvent( 421 | type=EventType.STATE_DELTA, 422 | delta=[ 423 | { 424 | "op": "add", 425 | "path": "/tool_logs/-", 426 | "value": { 427 | "message": "Gathering stock data", 428 | "status": "processing", 429 | "id": tool_log_id 430 | }, 431 | } 432 | ], 433 | ) 434 | ) 435 | await asyncio.sleep(0) 436 | 437 | # Step 2: Extract investment parameters from the last assistant message 438 | arguments = json.loads(state["messages"][-1].tool_calls[0].function.arguments) 439 | print("arguments", arguments) 440 | 441 | # Step 3: Update the investment portfolio in the state 442 | # Create portfolio entries with ticker symbols and investment amounts 443 | state["investment_portfolio"] = json.dumps( 444 | [ 445 | { 446 | "ticker": ticker, 447 | "amount": arguments["amount_of_dollars_to_be_invested"][index], 448 | } 449 | for index, ticker in enumerate(arguments["ticker_symbols"]) 450 | ] 451 | ) 452 | 453 | # Step 4: Emit state change event to update the UI 454 | config.get("configurable").get("emit_event")( 455 | StateDeltaEvent( 456 | type=EventType.STATE_DELTA, 457 | delta=[ 458 | { 459 | "op": "replace", 460 | "path": f"/investment_portfolio", 461 | "value": json.loads(state["investment_portfolio"]), 462 | } 463 | ], 464 | ) 465 | ) 466 | await asyncio.sleep(2) # Brief delay for UI updates 467 | 468 | # Step 5: Prepare parameters for historical data download 469 | tickers = arguments["ticker_symbols"] 470 | investment_date = arguments["investment_date"] 471 | current_year = datetime.now().year 472 | 473 | # Step 6: Validate and adjust investment date if necessary 474 | # Limit historical data to maximum of 4 years to avoid API limitations 475 | if( current_year - int(investment_date[:4]) > 4 ): 476 | print("investment date is more than 4 years ago") 477 | investment_date = f"{current_year - 4}-01-01" 478 | 479 | # Step 7: Determine the appropriate time period for data download 480 | if current_year - int(investment_date[:4]) == 0: 481 | history_period = "1y" # Current year - get 1 year of data 482 | else: 483 | history_period = f"{current_year - int(investment_date[:4])}y" 484 | 485 | # Step 8: Download historical stock data from Yahoo Finance 486 | data = yf.download( 487 | tickers, # List of ticker symbols 488 | period=history_period, # Time period for historical data 489 | interval="3mo", # Data interval (quarterly) 490 | start=investment_date, # Start date 491 | end=datetime.today().strftime("%Y-%m-%d"), # End date (today) 492 | ) 493 | 494 | # Step 9: Store the closing prices and arguments in state for next nodes 495 | state["be_stock_data"] = data["Close"] # Extract closing prices only 496 | state["be_arguments"] = arguments # Store parsed arguments 497 | print(state["be_stock_data"]) 498 | 499 | # Step 10: Update tool log status to completed 500 | index = len(state["tool_logs"]) - 1 501 | config.get("configurable").get("emit_event")( 502 | StateDeltaEvent( 503 | type=EventType.STATE_DELTA, 504 | delta=[ 505 | { 506 | "op": "replace", 507 | "path": f"/tool_logs/{index}/status", 508 | "value": "completed" 509 | } 510 | ], 511 | ) 512 | ) 513 | await asyncio.sleep(0) 514 | 515 | # Step 11: Direct workflow to the cash allocation node 516 | return Command(goto="cash_allocation", update=state) 517 | 518 | 519 | async def cash_allocation_node(state: AgentState, config: RunnableConfig): 520 | """ 521 | Third node in the workflow: Performs investment simulation and cash allocation. 522 | 523 | This is the most complex node that handles: 524 | 1. Investment simulation (single-shot vs dollar-cost averaging) 525 | 2. Cash allocation and share purchasing logic 526 | 3. Portfolio performance calculation vs SPY benchmark 527 | 4. Investment logging and error handling 528 | 5. UI data preparation for charts and tables 529 | 530 | Args: 531 | state: Current agent state with stock data and investment parameters 532 | config: Runtime configuration including event emitters 533 | 534 | Returns: 535 | Command: Directs workflow to the insights node 536 | """ 537 | print("inside cash allocation node") 538 | 539 | # Step 1: Create and emit tool log entry for UI feedback 540 | tool_log_id = str(uuid.uuid4()) 541 | state["tool_logs"].append( 542 | {"id": tool_log_id, "message": "Allocating cash", "status": "processing"} 543 | ) 544 | config.get("configurable").get("emit_event")( 545 | StateDeltaEvent( 546 | type=EventType.STATE_DELTA, 547 | delta=[ 548 | { 549 | "op": "add", 550 | "path": "/tool_logs/-", 551 | "value": { 552 | "message": "Allocating cash", 553 | "status": "processing", 554 | "id": tool_log_id 555 | }, 556 | } 557 | ], 558 | ) 559 | ) 560 | await asyncio.sleep(2) 561 | 562 | # Step 2: Import required libraries for numerical computations 563 | import numpy as np 564 | import pandas as pd 565 | 566 | # Step 3: Extract data from state for investment simulation 567 | stock_data = state["be_stock_data"] # DataFrame: index=date, columns=tickers 568 | args = state["be_arguments"] 569 | tickers = args["ticker_symbols"] 570 | investment_date = args["investment_date"] 571 | amounts = args["amount_of_dollars_to_be_invested"] # list, one per ticker 572 | interval = args.get("interval_of_investment", "single_shot") 573 | 574 | # Step 4: Initialize cash and portfolio tracking variables 575 | # Use state['available_cash'] as a single integer (total wallet cash) 576 | if "available_cash" in state and state["available_cash"] is not None: 577 | total_cash = state["available_cash"] 578 | else: 579 | total_cash = sum(amounts) # Fallback to sum of investment amounts 580 | 581 | # Initialize holdings dictionary to track shares owned 582 | holdings = {ticker: 0.0 for ticker in tickers} 583 | investment_log = [] # Log of all investment transactions 584 | add_funds_needed = False # Flag for insufficient funds 585 | add_funds_dates = [] # Dates where additional funds were needed 586 | 587 | # Step 5: Ensure stock data is sorted chronologically 588 | stock_data = stock_data.sort_index() 589 | 590 | # Step 6: Execute investment strategy based on interval type 591 | if interval == "single_shot": 592 | # Single-shot investment: Buy all shares at the first available date 593 | first_date = stock_data.index[0] 594 | row = stock_data.loc[first_date] 595 | 596 | # Process each ticker for single-shot investment 597 | for idx, ticker in enumerate(tickers): 598 | price = row[ticker] 599 | 600 | # Skip if no price data available 601 | if np.isnan(price): 602 | investment_log.append( 603 | f"{first_date.date()}: No price data for {ticker}, could not invest." 604 | ) 605 | add_funds_needed = True 606 | add_funds_dates.append( 607 | (str(first_date.date()), ticker, price, amounts[idx]) 608 | ) 609 | continue 610 | 611 | # Get allocated amount for this specific ticker 612 | allocated = amounts[idx] 613 | 614 | # Check if we have enough cash and the allocation covers at least one share 615 | if total_cash >= allocated and allocated >= price: 616 | shares_to_buy = allocated // price # Calculate shares (no fractional shares) 617 | if shares_to_buy > 0: 618 | cost = shares_to_buy * price 619 | holdings[ticker] += shares_to_buy 620 | total_cash -= cost 621 | investment_log.append( 622 | f"{first_date.date()}: Bought {shares_to_buy:.2f} shares of {ticker} at ${price:.2f} (cost: ${cost:.2f})" 623 | ) 624 | else: 625 | investment_log.append( 626 | f"{first_date.date()}: Not enough allocated cash to buy {ticker} at ${price:.2f}. Allocated: ${allocated:.2f}" 627 | ) 628 | add_funds_needed = True 629 | add_funds_dates.append( 630 | (str(first_date.date()), ticker, price, allocated) 631 | ) 632 | else: 633 | investment_log.append( 634 | f"{first_date.date()}: Not enough total cash to buy {ticker} at ${price:.2f}. Allocated: ${allocated:.2f}, Available: ${total_cash:.2f}" 635 | ) 636 | add_funds_needed = True 637 | add_funds_dates.append( 638 | (str(first_date.date()), ticker, price, total_cash) 639 | ) 640 | # No further purchases on subsequent dates for single-shot strategy 641 | else: 642 | # Dollar-Cost Averaging (DCA) or other interval-based investment strategy 643 | for date, row in stock_data.iterrows(): 644 | for i, ticker in enumerate(tickers): 645 | price = row[ticker] 646 | if np.isnan(price): 647 | continue # skip if price is NaN 648 | 649 | # Invest as much as possible for this ticker at this date 650 | if total_cash >= price: 651 | shares_to_buy = total_cash // price 652 | if shares_to_buy > 0: 653 | cost = shares_to_buy * price 654 | holdings[ticker] += shares_to_buy 655 | total_cash -= cost 656 | investment_log.append( 657 | f"{date.date()}: Bought {shares_to_buy:.2f} shares of {ticker} at ${price:.2f} (cost: ${cost:.2f})" 658 | ) 659 | else: 660 | add_funds_needed = True 661 | add_funds_dates.append( 662 | (str(date.date()), ticker, price, total_cash) 663 | ) 664 | investment_log.append( 665 | f"{date.date()}: Not enough cash to buy {ticker} at ${price:.2f}. Available: ${total_cash:.2f}. Please add more funds." 666 | ) 667 | 668 | # Step 7: Calculate final portfolio value and performance metrics 669 | final_prices = stock_data.iloc[-1] # Get the last available prices 670 | total_value = 0.0 671 | returns = {} 672 | total_invested_per_stock = {} 673 | percent_allocation_per_stock = {} 674 | percent_return_per_stock = {} 675 | total_invested = 0.0 676 | 677 | # Calculate how much was actually invested in each stock 678 | for idx, ticker in enumerate(tickers): 679 | if interval == "single_shot": 680 | # Only one purchase at first date for single-shot strategy 681 | first_date = stock_data.index[0] 682 | price = stock_data.loc[first_date][ticker] 683 | shares_bought = holdings[ticker] 684 | invested = shares_bought * price 685 | else: 686 | # Sum all purchases from the investment log for DCA strategy 687 | invested = 0.0 688 | for log in investment_log: 689 | if f"shares of {ticker}" in log and "Bought" in log: 690 | # Extract cost from log string 691 | try: 692 | cost_str = log.split("(cost: $")[-1].split(")")[0] 693 | invested += float(cost_str) 694 | except Exception: 695 | pass 696 | total_invested_per_stock[ticker] = invested 697 | total_invested += invested 698 | 699 | # Step 8: Calculate percentage allocations and returns for each stock 700 | for ticker in tickers: 701 | invested = total_invested_per_stock[ticker] 702 | holding_value = holdings[ticker] * final_prices[ticker] 703 | returns[ticker] = holding_value - invested # Absolute return in dollars 704 | total_value += holding_value 705 | 706 | # Calculate percentage allocation (how much of total was invested in this stock) 707 | percent_allocation_per_stock[ticker] = ( 708 | (invested / total_invested * 100) if total_invested > 0 else 0.0 709 | ) 710 | 711 | # Calculate percentage return for this stock 712 | percent_return_per_stock[ticker] = ( 713 | ((holding_value - invested) / invested * 100) if invested > 0 else 0.0 714 | ) 715 | 716 | total_value += total_cash # Add remaining cash to total portfolio value 717 | 718 | # Step 9: Store investment results in state for UI display 719 | state["investment_summary"] = { 720 | "holdings": holdings, # Shares owned per ticker 721 | "final_prices": final_prices.to_dict(), # Current stock prices 722 | "cash": total_cash, # Remaining cash 723 | "returns": returns, # Dollar returns per stock 724 | "total_value": total_value, # Total portfolio value 725 | "investment_log": investment_log, # Transaction history 726 | "add_funds_needed": add_funds_needed, # Whether more funds needed 727 | "add_funds_dates": add_funds_dates, # Dates funds were insufficient 728 | "total_invested_per_stock": total_invested_per_stock, # Amount invested per stock 729 | "percent_allocation_per_stock": percent_allocation_per_stock, # Allocation percentages 730 | "percent_return_per_stock": percent_return_per_stock, # Return percentages 731 | } 732 | state["available_cash"] = total_cash # Update available cash in state 733 | 734 | # Step 10: Portfolio vs SPY (S&P 500) benchmark comparison 735 | # Get SPY prices for the same date range to compare portfolio performance 736 | spy_ticker = "SPY" 737 | spy_prices = None 738 | try: 739 | spy_prices = yf.download( 740 | spy_ticker, 741 | period=f"{len(stock_data)//4}y" if len(stock_data) > 4 else "1y", 742 | interval="3mo", 743 | start=stock_data.index[0], 744 | end=stock_data.index[-1], 745 | )["Close"] 746 | # Align SPY prices to stock_data dates using forward fill 747 | spy_prices = spy_prices.reindex(stock_data.index, method="ffill") 748 | except Exception as e: 749 | print("Error fetching SPY data:", e) 750 | spy_prices = pd.Series([None] * len(stock_data), index=stock_data.index) 751 | 752 | # Step 11: Simulate investing the same total amount in SPY for comparison 753 | spy_shares = 0.0 754 | spy_cash = total_invested # Use same total investment amount 755 | spy_invested = 0.0 756 | spy_investment_log = [] 757 | 758 | if interval == "single_shot": 759 | # Single-shot SPY investment at first date 760 | first_date = stock_data.index[0] 761 | spy_price = spy_prices.loc[first_date] 762 | if isinstance(spy_price, pd.Series): 763 | spy_price = spy_price.iloc[0] 764 | if not pd.isna(spy_price): 765 | spy_shares = spy_cash // spy_price 766 | spy_invested = spy_shares * spy_price 767 | spy_cash -= spy_invested 768 | spy_investment_log.append( 769 | f"{first_date.date()}: Bought {spy_shares:.2f} shares of SPY at ${spy_price:.2f} (cost: ${spy_invested:.2f})" 770 | ) 771 | else: 772 | # DCA strategy for SPY: invest equal portions at each date 773 | dca_amount = total_invested / len(stock_data) 774 | for date in stock_data.index: 775 | spy_price = spy_prices.loc[date] 776 | if isinstance(spy_price, pd.Series): 777 | spy_price = spy_price.iloc[0] 778 | if not pd.isna(spy_price): 779 | shares = dca_amount // spy_price 780 | cost = shares * spy_price 781 | spy_shares += shares 782 | spy_cash -= cost 783 | spy_invested += cost 784 | spy_investment_log.append( 785 | f"{date.date()}: Bought {shares:.2f} shares of SPY at ${spy_price:.2f} (cost: ${cost:.2f})" 786 | ) 787 | 788 | # Step 12: Build performance comparison data for charting 789 | # Create time series data comparing portfolio vs SPY performance 790 | performanceData = [] 791 | running_holdings = holdings.copy() # Snapshot of final holdings 792 | running_cash = total_cash # Remaining cash 793 | 794 | for date in stock_data.index: 795 | # Calculate portfolio value at this historical date 796 | port_value = ( 797 | sum( 798 | running_holdings[t] * stock_data.loc[date][t] 799 | for t in tickers 800 | if not pd.isna(stock_data.loc[date][t]) 801 | ) 802 | + running_cash 803 | ) 804 | 805 | # Calculate SPY value at this historical date 806 | spy_price = spy_prices.loc[date] 807 | if isinstance(spy_price, pd.Series): 808 | spy_price = spy_price.iloc[0] 809 | spy_val = spy_shares * spy_price + spy_cash if not pd.isna(spy_price) else None 810 | 811 | # Add data point for this date 812 | performanceData.append( 813 | { 814 | "date": str(date.date()), 815 | "portfolio": float(port_value) if port_value is not None else None, 816 | "spy": float(spy_val) if spy_val is not None else None, 817 | } 818 | ) 819 | 820 | # Step 13: Add performance comparison data to investment summary 821 | state["investment_summary"]["performanceData"] = performanceData 822 | 823 | # Step 14: Generate summary message for the user 824 | if add_funds_needed: 825 | msg = "Some investments could not be made due to insufficient funds. Please add more funds to your wallet.\n" 826 | for d, t, p, c in add_funds_dates: 827 | msg += ( 828 | f"On {d}, not enough cash for {t}: price ${p:.2f}, available ${c:.2f}\n" 829 | ) 830 | else: 831 | msg = "All investments were made successfully.\n" 832 | 833 | msg += f"\nFinal portfolio value: ${total_value:.2f}\n" 834 | msg += "Returns by ticker (percent and $):\n" 835 | for ticker in tickers: 836 | percent = percent_return_per_stock[ticker] 837 | abs_return = returns[ticker] 838 | msg += f"{ticker}: {percent:.2f}% (${abs_return:.2f})\n" 839 | 840 | # Step 15: Add tool result message to conversation 841 | state["messages"].append( 842 | ToolMessageAGUI( 843 | role="tool", 844 | id=str(uuid.uuid4()), 845 | content="The relevant details had been extracted", 846 | tool_call_id=state["messages"][-1].tool_calls[0].id, 847 | ) 848 | ) 849 | 850 | # Step 16: Add assistant message with chart rendering tool call 851 | state["messages"].append( 852 | AssistantMessage( 853 | role="assistant", 854 | tool_calls=[ 855 | { 856 | "id": str(uuid.uuid4()), 857 | "type": "function", 858 | "function": { 859 | "name": "render_standard_charts_and_table", 860 | "arguments": json.dumps( 861 | {"investment_summary": state["investment_summary"]} 862 | ), 863 | }, 864 | } 865 | ], 866 | id=str(uuid.uuid4()), 867 | ) 868 | ) 869 | 870 | # Step 17: Update tool log status to completed 871 | index = len(state["tool_logs"]) - 1 872 | config.get("configurable").get("emit_event")( 873 | StateDeltaEvent( 874 | type=EventType.STATE_DELTA, 875 | delta=[ 876 | { 877 | "op": "replace", 878 | "path": f"/tool_logs/{index}/status", 879 | "value": "completed" 880 | } 881 | ], 882 | ) 883 | ) 884 | await asyncio.sleep(0) 885 | 886 | # Step 18: Direct workflow to the insights generation node 887 | return Command(goto="ui_decision", update=state) 888 | 889 | async def insights_node(state: AgentState, config: RunnableConfig): 890 | """ 891 | Fourth node in the workflow: Generates investment insights using AI. 892 | 893 | This function: 894 | 1. Creates a tool log entry for UI feedback 895 | 2. Extracts ticker symbols from the investment arguments 896 | 3. Uses Gemini model to generate bull and bear insights 897 | 4. Integrates insights into the existing tool call arguments 898 | 5. Updates the conversation state with the enhanced data 899 | 900 | Args: 901 | state: Current agent state with investment data and analysis 902 | config: Runtime configuration including event emitters 903 | 904 | Returns: 905 | Command: Directs workflow to the end node 906 | """ 907 | print("inside insights node") 908 | 909 | # Step 1: Create and emit tool log entry for UI feedback 910 | tool_log_id = str(uuid.uuid4()) 911 | state["tool_logs"].append( 912 | {"id": tool_log_id, "message": "Extracting key insights", "status": "processing"} 913 | ) 914 | config.get("configurable").get("emit_event")( 915 | StateDeltaEvent( 916 | type=EventType.STATE_DELTA, 917 | delta=[ 918 | { 919 | "op": "add", 920 | "path": "/tool_logs/-", 921 | "value": { 922 | "message": "Extracting key insights", 923 | "status": "processing", 924 | "id": tool_log_id 925 | }, 926 | } 927 | ], 928 | ) 929 | ) 930 | await asyncio.sleep(0) 931 | 932 | # Step 2: Extract ticker symbols from investment arguments 933 | args = state.get("be_arguments") or state.get("arguments") 934 | tickers = args.get("ticker_symbols", []) 935 | 936 | # Step 3: Initialize AI model and generate insights 937 | model = init_chat_model("gemini-2.5-pro", model_provider="google_genai") 938 | response = await model.bind_tools(generate_insights).ainvoke( 939 | [ 940 | {"role": "system", "content": insights_prompt}, 941 | {"role": "user", "content": tickers}, # Send ticker list to model 942 | ], 943 | config=config, 944 | ) 945 | 946 | # Step 4: Process the insights response 947 | if response.tool_calls: 948 | # Step 4a: Extract current arguments from the last tool call 949 | args_dict = json.loads(state["messages"][-1].tool_calls[0].function.arguments) 950 | 951 | # Step 4b: Add the generated insights to the arguments 952 | args_dict["insights"] = response.tool_calls[0]["args"] 953 | 954 | # Step 4c: Update the tool call arguments with insights included 955 | state["messages"][-1].tool_calls[0].function.arguments = json.dumps(args_dict) 956 | else: 957 | # Step 4d: Handle case where no insights were generated 958 | state["insights"] = {} 959 | 960 | # Step 5: Update tool log status to completed 961 | index = len(state["tool_logs"]) - 1 962 | config.get("configurable").get("emit_event")( 963 | StateDeltaEvent( 964 | type=EventType.STATE_DELTA, 965 | delta=[ 966 | { 967 | "op": "replace", 968 | "path": f"/tool_logs/{index}/status", 969 | "value": "completed" 970 | } 971 | ], 972 | ) 973 | ) 974 | await asyncio.sleep(0) 975 | 976 | # Step 6: Direct workflow to the end node (completion) 977 | return Command(goto="end", update=state) 978 | 979 | 980 | def router_function1(state: AgentState, config: RunnableConfig): 981 | """ 982 | Router function that determines the next node in the workflow. 983 | 984 | This function examines the last message in the conversation to decide 985 | whether to proceed to the simulation node or end the workflow. 986 | 987 | Args: 988 | state: Current agent state with conversation messages 989 | config: Runtime configuration (unused in this router) 990 | 991 | Returns: 992 | str: Next node name ("end" or "simulation") 993 | """ 994 | # Check if the last message has tool calls 995 | if ( 996 | state["messages"][-1].tool_calls == [] 997 | or state["messages"][-1].tool_calls is None 998 | ): 999 | # No tool calls means end the workflow (likely a text response) 1000 | return "end" 1001 | else: 1002 | # Tool calls present means proceed to simulation 1003 | return "simulation" 1004 | 1005 | 1006 | async def agent_graph(): 1007 | """ 1008 | Creates and configures the LangGraph workflow for stock analysis. 1009 | 1010 | This function: 1011 | 1. Creates a StateGraph with the AgentState structure 1012 | 2. Adds all workflow nodes (chat, simulation, cash_allocation, insights, end) 1013 | 3. Defines the workflow edges and conditional routing 1014 | 4. Sets entry and exit points 1015 | 5. Compiles the graph for execution 1016 | 1017 | Returns: 1018 | CompiledStateGraph: The compiled workflow ready for execution 1019 | """ 1020 | # Step 1: Create the workflow graph with AgentState structure 1021 | workflow = StateGraph(AgentState) 1022 | 1023 | # Step 2: Add all nodes to the workflow 1024 | workflow.add_node("chat", chat_node) # Initial chat and parameter extraction 1025 | workflow.add_node("simulation", simulation_node) # Stock data gathering 1026 | workflow.add_node("cash_allocation", cash_allocation_node) # Investment simulation and analysis 1027 | workflow.add_node("insights", insights_node) # AI-generated insights 1028 | workflow.add_node("end", end_node) # Terminal node 1029 | 1030 | # Step 3: Set workflow entry and exit points 1031 | workflow.set_entry_point("chat") # Always start with chat node 1032 | workflow.set_finish_point("end") # Always end with end node 1033 | 1034 | # Step 4: Define workflow edges and routing logic 1035 | workflow.add_edge(START, "chat") # Entry: START -> chat 1036 | workflow.add_conditional_edges("chat", router_function1) # Conditional: chat -> (simulation|end) 1037 | workflow.add_edge("simulation", "cash_allocation") # Direct: simulation -> cash_allocation 1038 | workflow.add_edge("cash_allocation", "insights") # Direct: cash_allocation -> insights 1039 | workflow.add_edge("insights", "end") # Direct: insights -> end 1040 | workflow.add_edge("end", END) # Exit: end -> END 1041 | 1042 | # Step 5: Compile the workflow graph 1043 | # Note: Memory persistence is commented out for simplicity 1044 | # from langgraph.checkpoint.memory import MemorySaver 1045 | # graph = workflow.compile(MemorySaver()) 1046 | graph = workflow.compile() 1047 | 1048 | return graph 1049 | --------------------------------------------------------------------------------