├── 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 |
23 | Ticker
24 |
25 | %
26 |
27 | Value
28 |
29 |
30 | Return
31 |
32 |
33 |
34 |
35 | {allocations?.map((allocation, index) => (
36 |
37 |
38 | {allocation.ticker}
39 |
40 | {allocation.allocation.toFixed(2)}%
41 |
42 | ${(allocation.currentValue / 1000).toFixed(1)}K
43 |
44 |
45 | = 0 ? "text-[#1B606F]" : "text-red-600"}>
46 | {allocation.totalReturn >= 0 ? "+" : ""}
47 | {allocation.totalReturn.toFixed(1)}%
48 |
49 |
50 |
51 | ))}
52 |
53 |
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 |
115 |
116 |
117 | {sandBoxPortfolio?.length === 0 ? (
118 |
No performance data to show.
119 | ) : (
120 |
({
123 | ...d,
124 | portfolio: d.portfolio ?? 0,
125 | spy: d.spy ?? 0,
126 | }))
127 | }
128 | />
129 | )}
130 |
131 |
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 |
74 |
75 |
76 |
77 |
78 |
79 |
80 | ) : (
81 |
82 |
83 | {formatCurrency(totalCash)}
84 |
85 |
89 |
90 |
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 |
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 | {
150 | debugger;
151 | if (respond) {
152 | setTotalCash(args?.investment_summary?.cash);
153 | setCurrentState({
154 | ...currentState,
155 | returnsData: Object.entries(
156 | args?.investment_summary?.percent_return_per_stock
157 | ).map(([ticker, return1]) => ({
158 | ticker,
159 | return: return1 as number,
160 | })),
161 | allocations: Object.entries(
162 | args?.investment_summary?.percent_allocation_per_stock
163 | ).map(([ticker, allocation]) => ({
164 | ticker,
165 | allocation: allocation as number,
166 | currentValue:
167 | args?.investment_summary?.final_prices[ticker] *
168 | args?.investment_summary?.holdings[ticker],
169 | totalReturn:
170 | args?.investment_summary?.percent_return_per_stock[
171 | ticker
172 | ],
173 | })),
174 | performanceData:
175 | args?.investment_summary?.performanceData,
176 | bullInsights: args?.insights?.bullInsights || [],
177 | bearInsights: args?.insights?.bearInsights || [],
178 | currentPortfolioValue:
179 | args?.investment_summary?.total_value,
180 | totalReturns: (
181 | Object.values(
182 | args?.investment_summary?.returns
183 | ) as number[]
184 | ).reduce((acc, val) => acc + val, 0),
185 | });
186 | setInvestedAmount(
187 | (
188 | Object.values(
189 | args?.investment_summary?.total_invested_per_stock
190 | ) as number[]
191 | ).reduce((acc, val) => acc + val, 0)
192 | );
193 | setState({
194 | ...state,
195 | available_cash: totalCash,
196 | });
197 | respond(
198 | "Data rendered successfully. Provide summary of the investments by not making any tool calls"
199 | );
200 | }
201 | }}>
202 | Accept
203 |
204 | {
208 | debugger;
209 | if (respond) {
210 | respond(
211 | "Data rendering rejected. Just give a summary of the rejected investments by not making any tool calls"
212 | );
213 | }
214 | }}>
215 | Reject
216 |
217 | >
218 | )}
219 | >
220 | );
221 | },
222 | });
223 |
224 | useCopilotAction({
225 | name: "render_custom_charts",
226 | renderAndWaitForResponse: ({ args, respond, status }) => {
227 | return (
228 | <>
229 |
233 | {
237 | debugger;
238 | if (respond) {
239 | setSandBoxPortfolio([
240 | ...sandBoxPortfolio,
241 | {
242 | performanceData:
243 | args?.investment_summary?.performanceData.map(
244 | (item: any) => ({
245 | date: item.date,
246 | portfolio: item.portfolio,
247 | spy: 0,
248 | })
249 | ) || [],
250 | },
251 | ]);
252 | respond(
253 | "Data rendered successfully. Provide summary of the investments"
254 | );
255 | }
256 | }}>
257 | Accept
258 |
259 | {
263 | debugger;
264 | if (respond) {
265 | respond(
266 | "Data rendering rejected. Just give a summary of the rejected investments"
267 | );
268 | }
269 | }}>
270 | Reject
271 |
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 |
332 |
333 | {/* Center Panel - Generative Canvas */}
334 |
335 | {/* Top Bar with Cash Info */}
336 |
337 |
346 |
347 |
348 | {/*
349 |
353 | {showComponentTree ? "Hide Tree" : "Show Tree"}
354 |
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 |
--------------------------------------------------------------------------------