├── .npmrc ├── .eslintrc.json ├── llm ├── components │ ├── forecasts │ │ ├── index.ts │ │ ├── documents.mdx │ │ └── documents.tsx │ ├── events │ │ ├── index.ts │ │ ├── events-skeleton.tsx │ │ ├── events.mdx │ │ └── events.tsx │ ├── simple-message.tsx │ ├── FormattedText.tsx │ ├── stocks │ │ ├── index.ts │ │ ├── stocks-skeleton.tsx │ │ ├── stock-purchase-status.tsx │ │ ├── conditional-purchase.mdx │ │ ├── stock-skeleton.tsx │ │ ├── positions.tsx │ │ └── stocks.tsx │ ├── not-available-read-only.tsx │ ├── serialization.tsx │ ├── warning-wrapper.tsx │ └── prompt-user-container.tsx ├── actions │ ├── history.tsx │ ├── conditional-purchases.ts │ ├── langchain-helpers.ts │ ├── newsletter.ts │ └── calendar-events.ts ├── tools │ ├── README.md │ ├── newsletter │ │ ├── check-subscription.tsx │ │ └── set-subscription.tsx │ ├── profile │ │ ├── set-profile-attributes.tsx │ │ ├── set-employeer.tsx │ │ └── get-profile.tsx │ ├── trading │ │ ├── show-current-positions.tsx │ │ ├── list-stocks.tsx │ │ └── show-stock-price.tsx │ └── events │ │ └── get-events.tsx ├── ai-params.ts ├── types.ts ├── utils.ts ├── system-prompt.ts └── ai-helpers.ts ├── app ├── favicon.ico ├── api │ ├── auth │ │ ├── user │ │ │ └── accounts │ │ │ │ ├── route.ts │ │ │ │ └── [provider] │ │ │ │ └── [user_id] │ │ │ │ └── route.ts │ │ └── [auth0] │ │ │ └── route.ts │ └── hooks │ │ └── route.ts ├── profile │ ├── components │ │ └── profile-page.tsx │ ├── layout.tsx │ └── page.tsx ├── report │ └── [id] │ │ ├── layout.tsx │ │ └── page.tsx ├── new │ └── page.tsx ├── chat │ └── [id] │ │ ├── page.tsx │ │ └── layout.tsx ├── page.tsx ├── manifest.ts ├── global-error.tsx ├── layout.tsx ├── docs │ └── [id] │ │ └── page.tsx ├── globals.css ├── read │ └── [id] │ │ └── layout.tsx └── actions.tsx ├── .fossa.yml ├── database └── market0 │ ├── V015__add_title_to_conv.sql │ ├── V010__remove_link_from_docs.sql │ ├── V014__remove_reminders.sql │ ├── V011__replace_real_names.sql │ ├── V017__add_is_public_to_conv.sql │ ├── V003__add_tx_owner.sql │ ├── V008__create_tokens_usage_table.sql │ ├── V004__add_chat_history.sql │ ├── V002__create_transaction_table.sql │ ├── V005__add_reminders_table.sql │ ├── V013__add_chat_users.sql │ ├── V016__refactor_chats.sql │ ├── V006__add_conditional_purchases_table.sql │ └── V001__initial.sql ├── postcss.config.mjs ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── sdk ├── components │ ├── loader.tsx │ ├── connect-google-account.tsx │ └── ensure-api-access.tsx ├── auth0 │ ├── 3rd-party-apis │ │ ├── providers │ │ │ ├── box.ts │ │ │ └── google.ts │ │ └── index.tsx │ └── mgmt.ts └── fga │ ├── next │ └── with-check-permission.tsx │ ├── index.ts │ └── langchain │ └── rag.ts ├── lib ├── db │ ├── index.ts │ ├── sql.ts │ ├── transactions.ts │ ├── documents.ts │ ├── userUsage.ts │ ├── conditional-purchases.ts │ └── chat-users.ts ├── constants.ts ├── utils.ts ├── market │ └── stocks.ts ├── documents.ts └── examples.tsx ├── components ├── loader.tsx ├── theme-provider.tsx ├── ui │ ├── label.tsx │ ├── input.tsx │ ├── separator.tsx │ ├── toaster.tsx │ ├── tooltip.tsx │ ├── badge.tsx │ ├── popover.tsx │ ├── avatar.tsx │ ├── scroll-area.tsx │ ├── button.tsx │ ├── card.tsx │ └── drawer.tsx ├── explanation │ └── observable.tsx ├── chat │ ├── header.tsx │ ├── context.tsx │ └── share │ │ ├── users-permissions-list.tsx │ │ └── user-permission-actions.tsx ├── auth0 │ ├── basic-info-form.tsx │ └── user-button.tsx ├── welcome │ └── Welcome.tsx ├── with-toolbar.tsx └── fga │ └── error.tsx ├── instrumentation.ts ├── components.json ├── middleware.ts ├── .github └── workflows │ └── db.yml ├── .gitignore ├── sentry.server.config.ts ├── sentry.client.config.ts ├── sentry.edge.config.ts ├── tsconfig.json ├── .env-example ├── hooks ├── chat │ ├── use-copy-to-clipboard.tsx │ └── use-scroll-to-bottom.tsx └── auth0 │ ├── helpers │ └── rate-limit.ts │ └── use-connected-accounts.ts ├── docker-compose.yml ├── scripts ├── fga │ └── syncDocs.ts └── llm │ ├── insert-forecasts.ts │ └── insert-earnings.ts ├── README.md ├── next.config.mjs ├── mdx-components.tsx ├── routers └── user-accounts.ts ├── CONTRIBUTING.md ├── package.json └── tailwind.config.ts /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /llm/components/forecasts/index.ts: -------------------------------------------------------------------------------- 1 | export { Documents } from "./documents"; 2 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auth0-lab/market0/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /.fossa.yml: -------------------------------------------------------------------------------- 1 | version: 3 2 | 3 | paths: 4 | exclude: 5 | - ./.next 6 | - ./.vercel 7 | -------------------------------------------------------------------------------- /database/market0/V015__add_title_to_conv.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE chat_histories 2 | ADD COLUMN title TEXT; 3 | 4 | -------------------------------------------------------------------------------- /database/market0/V010__remove_link_from_docs.sql: -------------------------------------------------------------------------------- 1 | UPDATE 2 | documents 3 | SET 4 | metadata = metadata - 'link'; 5 | 6 | -------------------------------------------------------------------------------- /llm/components/events/index.ts: -------------------------------------------------------------------------------- 1 | export { Events } from "./events"; 2 | export { EventsSkeleton } from "./events-skeleton"; 3 | -------------------------------------------------------------------------------- /database/market0/V014__remove_reminders.sql: -------------------------------------------------------------------------------- 1 | DROP FUNCTION "update_reminders_updated_at"; 2 | 3 | DROP TABLE "reminders"; 4 | 5 | -------------------------------------------------------------------------------- /database/market0/V011__replace_real_names.sql: -------------------------------------------------------------------------------- 1 | UPDATE 2 | documents 3 | SET 4 | content = REPLACE(content, 'NASDAQ', 'NMS'); 5 | 6 | -------------------------------------------------------------------------------- /llm/components/simple-message.tsx: -------------------------------------------------------------------------------- 1 | export const SimpleMessage = ({text} : {text: string}) => { 2 | return
{text}
3 | }; 4 | -------------------------------------------------------------------------------- /app/api/auth/user/accounts/route.ts: -------------------------------------------------------------------------------- 1 | import { handleUserAccountsFetch } from "@/routers/user-accounts"; 2 | 3 | export const GET = handleUserAccountsFetch(); 4 | -------------------------------------------------------------------------------- /app/api/auth/user/accounts/[provider]/[user_id]/route.ts: -------------------------------------------------------------------------------- 1 | import { handleDeleteUserAccount } from "@/routers/user-accounts"; 2 | 3 | export const DELETE = handleDeleteUserAccount(); 4 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /database/market0/V017__add_is_public_to_conv.sql: -------------------------------------------------------------------------------- 1 | /* This columns is only used to prevent the deletion of chats. */ 2 | ALTER TABLE conversations 3 | ADD COLUMN is_public BOOLEAN NOT NULL DEFAULT FALSE; 4 | -------------------------------------------------------------------------------- /database/market0/V003__add_tx_owner.sql: -------------------------------------------------------------------------------- 1 | TRUNCATE TABLE "transactions"; 2 | 3 | ALTER TABLE 4 | "transactions" 5 | ADD 6 | COLUMN "user_id" text NOT NULL; 7 | 8 | CREATE INDEX ON "transactions" (user_id); 9 | -------------------------------------------------------------------------------- /llm/components/FormattedText.tsx: -------------------------------------------------------------------------------- 1 | import Markdown from "react-markdown"; 2 | 3 | export const FormattedText = ({content} : { content: string}) => { 4 | return {content}; 5 | }; 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "mikerhyssmith.ts-barrelr", 4 | "mike-co.import-sorter", 5 | "editorconfig.editorconfig", 6 | "esbenp.prettier-vscode", 7 | "Tobermory.es6-string-html" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /sdk/components/loader.tsx: -------------------------------------------------------------------------------- 1 | export default function Loader() { 2 | return ( 3 |
4 |
5 |
6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /app/profile/components/profile-page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import UserProfile from "@/components/auth0/user-profile"; 4 | import { Claims } from "@auth0/nextjs-auth0"; 5 | 6 | export function ProfilePage({ user }: { user: Claims }) { 7 | return ; 8 | } 9 | -------------------------------------------------------------------------------- /llm/actions/history.tsx: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { conversations } from "@/lib/db"; 4 | import { getUser } from "@/sdk/fga"; 5 | 6 | export const listUserConversations = async () => { 7 | const user = await getUser(); 8 | return await conversations.list({ ownerID: user.sub }); 9 | }; 10 | -------------------------------------------------------------------------------- /lib/db/index.ts: -------------------------------------------------------------------------------- 1 | export * as conditionalPurchases from "./conditional-purchases"; 2 | export * as userUsage from "./userUsage"; 3 | export * as documents from "./documents"; 4 | export * as chatUsers from "./chat-users"; 5 | export * as transactions from "./transactions"; 6 | export * as conversations from "./conversations"; 7 | -------------------------------------------------------------------------------- /app/profile/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Header } from "@/components/chat/header"; 2 | 3 | type ProfileLayoutParams = Readonly<{ 4 | children: React.ReactNode; 5 | }>; 6 | 7 | export default async function ProfileLayout({ children }: ProfileLayoutParams) { 8 | return ( 9 | <> 10 |
11 | {children} 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /app/report/[id]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Header } from "@/components/chat/header"; 2 | 3 | type ReportLayoutParams = Readonly<{ 4 | children: React.ReactNode; 5 | }>; 6 | 7 | export default async function ReportLayout({ children }: ReportLayoutParams) { 8 | return ( 9 | <> 10 |
11 | {children} 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /components/loader.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | export default function Loader({ className }: { className?: string }) { 4 | return ( 5 |
6 |
7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /lib/constants.ts: -------------------------------------------------------------------------------- 1 | export enum OBJECT { 2 | STOCKS = "assets:stocks", 3 | } 4 | 5 | export enum RELATION { 6 | CAN_VIEW_DOCS = "can_view", 7 | CAN_BUY_STOCKS = "can_buy", 8 | } 9 | 10 | export const CLAIMS = { 11 | ROLE: "https://biro.com/claims/role", 12 | }; 13 | 14 | export const ROLES = { 15 | TRADER: "trader", 16 | VIEWER: "viewer", 17 | }; 18 | -------------------------------------------------------------------------------- /components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 4 | import { ThemeProviderProps } from "next-themes/dist/types"; 5 | import * as React from "react"; 6 | 7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 8 | return {children}; 9 | } 10 | -------------------------------------------------------------------------------- /app/new/page.tsx: -------------------------------------------------------------------------------- 1 | import { generateId } from "ai"; 2 | import { redirect } from "next/navigation"; 3 | 4 | import { conversations } from "@/lib/db"; 5 | import { getUser } from "@/sdk/fga"; 6 | 7 | export default async function Root() { 8 | const user = await getUser(); 9 | const conversationID = await conversations.create({ owner: user }); 10 | redirect(`/chat/${conversationID}`); 11 | } 12 | -------------------------------------------------------------------------------- /instrumentation.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/nextjs'; 2 | 3 | export async function register() { 4 | if (process.env.NEXT_RUNTIME === 'nodejs') { 5 | await import('./sentry.server.config'); 6 | } 7 | 8 | if (process.env.NEXT_RUNTIME === 'edge') { 9 | await import('./sentry.edge.config'); 10 | } 11 | } 12 | 13 | export const onRequestError = Sentry.captureRequestError; 14 | -------------------------------------------------------------------------------- /llm/actions/conditional-purchases.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { ConditionalPurchase, getByID } from "@/lib/db/conditional-purchases"; 4 | import { getUser } from "@/sdk/fga"; 5 | 6 | export async function getConditionalPurchaseById( 7 | id: string 8 | ): Promise { 9 | "use server"; 10 | const user = await getUser(); 11 | return getByID(user.sub, id); 12 | } 13 | -------------------------------------------------------------------------------- /database/market0/V008__create_tokens_usage_table.sql: -------------------------------------------------------------------------------- 1 | -- Create token_usage table 2 | CREATE TABLE token_usage( 3 | "id" serial PRIMARY KEY, 4 | "user_id" text NOT NULL, 5 | "timestamp" timestamptz NOT NULL DEFAULT NOW(), 6 | "tokens_used" int NOT NULL 7 | ); 8 | 9 | -- Create an index on timestamp for efficient querying 10 | CREATE INDEX idx_token_usage_timestamp ON token_usage(timestamp); 11 | 12 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /database/market0/V004__add_chat_history.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE chat_histories ( 2 | id BIGSERIAL PRIMARY KEY, 3 | user_id TEXT NOT NULL, 4 | conversation_id TEXT NOT NULL, 5 | messages jsonb NOT NULL, 6 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 7 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 8 | ); 9 | 10 | CREATE UNIQUE INDEX chat_histories_conversation_id_index ON chat_histories(user_id, conversation_id); 11 | -------------------------------------------------------------------------------- /llm/components/stocks/index.ts: -------------------------------------------------------------------------------- 1 | export { Stocks } from "./stocks"; 2 | export { StocksSkeleton } from "./stocks-skeleton"; 3 | export { Stock } from "./stock"; 4 | export { StockSkeleton } from "./stock-skeleton"; 5 | export { StockPurchase } from "./stock-purchase"; 6 | export { StockPurchaseStatus } from "./stock-purchase-status"; 7 | export { ConditionalPurchase } from "./conditional-purchase"; 8 | export { Positions } from "./positions"; 9 | -------------------------------------------------------------------------------- /app/chat/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Conversation from "@/components/chat/conversation"; 4 | import { ObservableProvider } from "@/components/explanation/observable"; 5 | 6 | // Allow streaming responses up to 30 seconds 7 | export const maxDuration = 60; 8 | 9 | export default function Page() { 10 | return ( 11 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /database/market0/V002__create_transaction_table.sql: -------------------------------------------------------------------------------- 1 | CREATE TYPE transaction_type AS ENUM ('buy', 'sell'); 2 | 3 | CREATE TABLE transactions ( 4 | "id" bigserial PRIMARY KEY, 5 | "ticker_id" text NOT NULL, 6 | "type" VARCHAR(4) CHECK (TYPE IN ('buy', 'sell')), 7 | "quantity" INTEGER NOT NULL, 8 | "price" DECIMAL(10, 2) NOT NULL, 9 | "created_at" timestamp NOT NULL DEFAULT NOW(), 10 | "updated_at" timestamp NOT NULL DEFAULT NOW() 11 | ); 12 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | 3 | import WelcomeScreen from "@/components/welcome/Welcome"; 4 | import { conversations } from "@/lib/db"; 5 | import { getUser } from "@/sdk/fga"; 6 | 7 | export default async function Root() { 8 | const user = await getUser(); 9 | if (!user) { 10 | return ; 11 | } 12 | const conversationID = await conversations.create({ owner: user }); 13 | redirect(`/chat/${conversationID}`); 14 | } 15 | -------------------------------------------------------------------------------- /llm/tools/README.md: -------------------------------------------------------------------------------- 1 | This directory contains various tools for the chatbot, organized into folders by category: 2 | 3 | - [newsletter](newsletter/): Tools for managing subscriptions to the newsletter. 4 | - [profile](profile/): Tools for managing user profiles. 5 | - [schedule](schedule/): Tools for managing scheduled events. 6 | - [trading](trading/): Tools for managing stock trading. 7 | 8 | Each folder contains specific functionalities to enhance the chatbot's capabilities in these areas. 9 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import { withMiddlewareAuthRequired } from "@auth0/nextjs-auth0/edge"; 2 | 3 | export default withMiddlewareAuthRequired(); 4 | 5 | export const config = { 6 | matcher: [ 7 | /* 8 | * Match all request paths except for the ones starting with: 9 | * - api (API routes) 10 | * - _next/static (static files) 11 | * - _next/image (image optimization files) 12 | * - favicon.ico (favicon file) 13 | */ 14 | "/((?!api|_next/static|_next/image|favicon.ico|docs|manifest.webmanifest|read/|$).*)", 15 | ], 16 | }; 17 | -------------------------------------------------------------------------------- /.github/workflows/db.yml: -------------------------------------------------------------------------------- 1 | name: Migrate DB 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | 7 | jobs: 8 | migrate: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - name: Run Flyway migrations 13 | uses: docker://flyway/flyway:10 14 | env: 15 | FLYWAY_URL: ${{secrets.DB_URL}} 16 | FLYWAY_USER: ${{secrets.DB_USER}} 17 | FLYWAY_PASSWORD: ${{secrets.DB_PASSWORD}} 18 | FLYWAY_LOCATIONS: filesystem:./database/market0 19 | with: 20 | args: migrate 21 | -------------------------------------------------------------------------------- /app/manifest.ts: -------------------------------------------------------------------------------- 1 | import { MetadataRoute } from "next"; 2 | 3 | export default function manifest(): MetadataRoute.Manifest { 4 | return { 5 | name: "Auth0 AI | Market0", 6 | short_name: "Market0", 7 | description: "Market0 is a demo app that showcases secure auth patterns for GenAI apps", 8 | start_url: "/", 9 | display: "standalone", 10 | orientation: "portrait", 11 | background_color: "#fff", 12 | theme_color: "#fff", 13 | icons: [ 14 | { 15 | src: "/favicon.ico", 16 | sizes: "any", 17 | type: "image/x-icon", 18 | }, 19 | ], 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | 8 | export const formatNumber = (value: number) => 9 | new Intl.NumberFormat("en-US", { 10 | style: "currency", 11 | currency: "USD", 12 | }).format(value); 13 | 14 | export const runAsyncFnWithoutBlocking = (fn: (...args: any) => Promise) => { 15 | fn(); 16 | }; 17 | 18 | export function getGoogleConnectionName() { 19 | return process.env.NEXT_PUBLIC_GOOGLE_CONNECTION_NAME || "google-oauth2"; 20 | } 21 | -------------------------------------------------------------------------------- /.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.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | 39 | # Sentry Config File 40 | .env.sentry-build-plugin 41 | -------------------------------------------------------------------------------- /sentry.server.config.ts: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry on the server. 2 | // The config you add here will be used whenever the server handles a request. 3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 4 | 5 | import * as Sentry from "@sentry/nextjs"; 6 | 7 | Sentry.init({ 8 | dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, 9 | 10 | // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. 11 | tracesSampleRate: 1, 12 | 13 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 14 | debug: false, 15 | }); 16 | -------------------------------------------------------------------------------- /lib/db/sql.ts: -------------------------------------------------------------------------------- 1 | import postgres from "postgres"; 2 | 3 | import { neon } from "@neondatabase/serverless"; 4 | 5 | const queryDebugger = (connection: number, query: string, parameters: any[], paramTypes: any[]) => { 6 | console.log('postgres:', { connection, query, parameters, paramTypes }); 7 | }; 8 | 9 | export const sql: ReturnType = process.env.USE_NEON 10 | ? (neon(process.env.DATABASE_URL as string) as unknown as ReturnType< 11 | typeof postgres 12 | >) 13 | : postgres({ 14 | connection: { 15 | TimeZone: process.env.PGTZ ?? "UTC", 16 | }, 17 | debug: process.env.DEBUG_QUERIES ? queryDebugger : undefined, 18 | }); 19 | -------------------------------------------------------------------------------- /sentry.client.config.ts: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry on the client. 2 | // The config you add here will be used whenever a users loads a page in their browser. 3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 4 | 5 | import * as Sentry from "@sentry/nextjs"; 6 | 7 | Sentry.init({ 8 | dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, 9 | 10 | // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. 11 | tracesSampleRate: 1, 12 | 13 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 14 | debug: false, 15 | }); 16 | -------------------------------------------------------------------------------- /llm/components/stocks/stocks-skeleton.tsx: -------------------------------------------------------------------------------- 1 | export const StocksSkeleton = () => { 2 | return ( 3 |
4 |
5 |
6 |
7 |
8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /llm/components/events/events-skeleton.tsx: -------------------------------------------------------------------------------- 1 | export const EventsSkeleton = () => { 2 | return ( 3 |
4 |
5 |
6 | {"xxxxx"} 7 |
8 |
9 | {"xxxxxxxxxxx"} 10 |
11 |
12 |
13 |
14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /database/market0/V005__add_reminders_table.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "reminders"( 2 | "id" bigserial PRIMARY KEY, 3 | "user_id" text NOT NULL, 4 | "title" text NOT NULL, 5 | "due" timestamp NOT NULL, 6 | "notes" text, 7 | "link" text, 8 | "google_task_id" text, 9 | "created_at" timestamp DEFAULT CURRENT_TIMESTAMP, 10 | "updated_at" timestamp DEFAULT CURRENT_TIMESTAMP 11 | ); 12 | 13 | CREATE INDEX ON "reminders"(user_id); 14 | 15 | -- create a trigger to modify updated_at on every update: 16 | CREATE OR REPLACE FUNCTION update_reminders_updated_at() 17 | RETURNS TRIGGER 18 | AS $$ 19 | BEGIN 20 | NEW.updated_at = NOW(); 21 | RETURN NEW; 22 | END; 23 | $$ 24 | LANGUAGE plpgsql; 25 | 26 | -------------------------------------------------------------------------------- /database/market0/V013__add_chat_users.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE chat_users ( 2 | id BIGSERIAL PRIMARY KEY, 3 | chat_id TEXT NOT NULL, 4 | email TEXT NOT NULL, 5 | user_id TEXT, 6 | access TEXT NOT NULL, 7 | status TEXT NOT NULL, 8 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 9 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 10 | ); 11 | 12 | CREATE UNIQUE INDEX chat_users_chat_id_email_index ON chat_users(chat_id, email); 13 | 14 | -- create a trigger to modify updated_at on every update: 15 | CREATE OR REPLACE FUNCTION update_conditional_purchases_updated_at() 16 | RETURNS TRIGGER 17 | AS $$ 18 | BEGIN 19 | NEW.updated_at = NOW(); 20 | RETURN NEW; 21 | END; 22 | $$ 23 | LANGUAGE plpgsql; 24 | -------------------------------------------------------------------------------- /llm/ai-params.ts: -------------------------------------------------------------------------------- 1 | import { bedrock } from "@ai-sdk/amazon-bedrock"; 2 | import { mistral } from "@ai-sdk/mistral"; 3 | import { openai } from "@ai-sdk/openai"; 4 | 5 | const providers = { 6 | 'openai': openai, 7 | 'mistral': mistral, 8 | 'bedrock': bedrock, 9 | } 10 | const provider: keyof typeof providers = 11 | process.env.LLM_PROVIDER as keyof typeof providers ?? 12 | 'openai'; 13 | 14 | if (!Object.keys(providers).includes(provider)) { 15 | throw new Error(`LLM_PROVIDER must be set to: ${Object.keys(providers).join(', ')}`); 16 | } 17 | 18 | const model = process.env.LLM_MODEL ?? 'gpt-4o-mini'; 19 | 20 | export const aiParams = { 21 | model: providers[provider](model), 22 | temperature: 0.2, 23 | }; 24 | -------------------------------------------------------------------------------- /database/market0/V016__refactor_chats.sql: -------------------------------------------------------------------------------- 1 | -- Rename the table 2 | ALTER TABLE chat_histories RENAME TO conversations; 3 | 4 | -- Drop the unique index 5 | DROP INDEX IF EXISTS chat_histories_conversation_id_index; 6 | 7 | -- Drop the primary key constraint and the id column 8 | ALTER TABLE conversations 9 | DROP CONSTRAINT chat_histories_pkey; 10 | 11 | ALTER TABLE conversations 12 | DROP COLUMN id; 13 | 14 | -- Rename conversation_id to id 15 | ALTER TABLE conversations RENAME COLUMN conversation_id TO id; 16 | 17 | -- Rename user_id to owner_id 18 | ALTER TABLE conversations RENAME COLUMN user_id TO owner_id; 19 | 20 | -- Add the primary key on id only 21 | ALTER TABLE conversations 22 | ADD PRIMARY KEY (id); 23 | 24 | -------------------------------------------------------------------------------- /app/global-error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as Sentry from "@sentry/nextjs"; 4 | import NextError from "next/error"; 5 | import { useEffect } from "react"; 6 | 7 | export default function GlobalError({ error }: { error: Error & { digest?: string } }) { 8 | useEffect(() => { 9 | Sentry.captureException(error); 10 | }, [error]); 11 | 12 | return ( 13 | 14 | 15 | {/* `NextError` is the default Next.js error page component. Its type 16 | definition requires a `statusCode` prop. However, since the App Router 17 | does not expose status codes for errors, we simply pass 0 to render a 18 | generic error message. */} 19 | 20 | 21 | 22 | ); 23 | } -------------------------------------------------------------------------------- /llm/components/stocks/stock-purchase-status.tsx: -------------------------------------------------------------------------------- 1 | import { CancelRedIcon, CheckGreenIcon } from "@/components/icons"; 2 | 3 | export const StockPurchaseStatus = ({ 4 | message, 5 | status, 6 | }: { 7 | message: string; 8 | status?: "in-progress" | "success" | "failure"; 9 | }) => ( 10 |
11 | {status === "failure" && ( 12 |
13 | 14 |
15 | )} 16 | {status === "success" && ( 17 |
18 | 19 |
20 | )} 21 | {status === "in-progress" && ( 22 |
23 | )} 24 |

{message}

25 |
26 | ); 27 | -------------------------------------------------------------------------------- /sentry.edge.config.ts: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on). 2 | // The config you add here will be used whenever one of the edge features is loaded. 3 | // Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally. 4 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 5 | 6 | import * as Sentry from "@sentry/nextjs"; 7 | 8 | Sentry.init({ 9 | dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, 10 | 11 | // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. 12 | tracesSampleRate: 1, 13 | 14 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 15 | debug: false, 16 | }); 17 | -------------------------------------------------------------------------------- /llm/components/not-available-read-only.tsx: -------------------------------------------------------------------------------- 1 | import { WarningPageIcon } from "@/components/icons"; 2 | import { cn } from "@/lib/utils"; 3 | 4 | import { PromptUserContainer } from "./prompt-user-container"; 5 | 6 | export function NotAvailableReadOnly({ 7 | message, 8 | containerClassName, 9 | }: { 10 | message?: React.ReactNode; 11 | containerClassName?: string; 12 | }) { 13 | return ( 14 | 19 | 20 |
21 | } 22 | containerClassName={cn("border-gray-200 border-dashed bg-gray-100", containerClassName)} 23 | /> 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /database/market0/V006__add_conditional_purchases_table.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "conditional_purchases"( 2 | "id" bigserial PRIMARY KEY, 3 | "user_id" text NOT NULL, 4 | "symbol" text NOT NULL, 5 | "quantity" integer NOT NULL, 6 | "metric" text NOT NULL, 7 | "operator" text NOT NULL, 8 | "threshold" DECIMAL(10, 2) NOT NULL, 9 | "status" text NOT NULL, 10 | "link" text, 11 | "created_at" timestamp DEFAULT CURRENT_TIMESTAMP, 12 | "updated_at" timestamp DEFAULT CURRENT_TIMESTAMP 13 | ); 14 | 15 | CREATE INDEX ON "conditional_purchases"(user_id); 16 | 17 | -- create a trigger to modify updated_at on every update: 18 | CREATE OR REPLACE FUNCTION update_conditional_purchases_updated_at() 19 | RETURNS TRIGGER 20 | AS $$ 21 | BEGIN 22 | NEW.updated_at = NOW(); 23 | RETURN NEW; 24 | END; 25 | $$ 26 | LANGUAGE plpgsql; 27 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /database/market0/V001__initial.sql: -------------------------------------------------------------------------------- 1 | CREATE extension vector; 2 | 3 | CREATE TABLE documents ( 4 | id bigserial PRIMARY KEY, 5 | content text, 6 | metadata jsonb, 7 | embedding vector(1536) 8 | ); 9 | 10 | create or replace function match_documents ( 11 | query_embedding vector(1536), 12 | match_threshold float, 13 | match_count int 14 | ) 15 | returns table ( 16 | id bigint, 17 | content text, 18 | similarity float 19 | ) 20 | language sql stable 21 | as $$ 22 | select 23 | documents.id, 24 | documents.content, 25 | 1 - (documents.embedding <=> query_embedding) as similarity 26 | from documents 27 | where 1 - (documents.embedding <=> query_embedding) > match_threshold 28 | order by similarity desc 29 | limit match_count; 30 | $$; 31 | 32 | 33 | create index on documents using ivfflat (embedding vector_cosine_ops) 34 | with 35 | (lists = 100); 36 | -------------------------------------------------------------------------------- /app/profile/page.tsx: -------------------------------------------------------------------------------- 1 | import { ChevronLeftIcon } from "lucide-react"; 2 | 3 | import { getSession } from "@auth0/nextjs-auth0"; 4 | 5 | import { ProfilePage } from "./components/profile-page"; 6 | 7 | export default async function Profile() { 8 | const session = await getSession(); 9 | const user = session!.user; 10 | 11 | return ( 12 |
13 |
14 |
15 | 21 |
22 | 23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ); 21 | } 22 | ); 23 | Input.displayName = "Input"; 24 | 25 | export { Input }; 26 | -------------------------------------------------------------------------------- /llm/components/serialization.tsx: -------------------------------------------------------------------------------- 1 | import { Events } from "./events"; 2 | import { Documents } from "./forecasts/documents"; 3 | import { FormattedText } from "./FormattedText"; 4 | import { ProfileCard } from "./profile"; 5 | import { SimpleMessage } from "./simple-message"; 6 | import { ConditionalPurchase, Positions, Stock, StockPurchase, Stocks } from "./stocks"; 7 | 8 | export const components = { 9 | Documents, 10 | Events, 11 | Positions, 12 | Stock, 13 | Stocks, 14 | SimpleMessage, 15 | StockPurchase, 16 | ConditionalPurchase, 17 | FormattedText, 18 | ProfileCard, 19 | }; 20 | 21 | type ComponentsNames = keyof typeof components; 22 | type ComponentClasses = (typeof components)[ComponentsNames]; 23 | 24 | export const names = new Map( 25 | Object.entries(components).map(([name, component]) => [component, name as ComponentsNames]) 26 | ); 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "ts-node": { 25 | "compilerOptions": { 26 | "module": "NodeNext", 27 | "moduleResolution": "NodeNext" 28 | }, 29 | "require": [ 30 | "tsconfig-paths/register" 31 | ] 32 | }, 33 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 34 | "exclude": ["node_modules"] 35 | } 36 | -------------------------------------------------------------------------------- /components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Separator = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >( 12 | ( 13 | { className, orientation = "horizontal", decorative = true, ...props }, 14 | ref 15 | ) => ( 16 | 27 | ) 28 | ); 29 | Separator.displayName = SeparatorPrimitive.Root.displayName; 30 | 31 | export { Separator }; 32 | -------------------------------------------------------------------------------- /.env-example: -------------------------------------------------------------------------------- 1 | # Local DB 2 | PGUSER=postgres 3 | PGPASSWORD=postgres 4 | PGDATABASE=market0 5 | PGPORT=5442 6 | PGTZ=UTC 7 | 8 | ### ---------------- ### 9 | ### Production like ### 10 | ### ---------------- ### 11 | # Database 12 | DATABASE_URL=postgresql://..... 13 | 14 | # OpenAI 15 | OPENAI_API_KEY=.... 16 | 17 | # Auth0 18 | AUTH0_SECRET=LONG_RANDOM_VALUE 19 | AUTH0_BASE_URL=http://localhost:3000 20 | AUTH0_ISSUER_BASE_URL=https://TENANT.auth0lab.com 21 | AUTH0_CLIENT_ID=.... 22 | AUTH0_CLIENT_SECRET=.... 23 | AUTH0_CLIENT_ID_MGMT=.... 24 | AUTH0_CLIENT_SECRET_MGMT=.... 25 | 26 | # OKTA FGA 27 | FGA_STORE_ID=... 28 | FGA_MODEL_ID=... 29 | FGA_CLIENT_ID=... 30 | FGA_CLIENT_SECRET=... 31 | FGA_API_URL="https://api.us1.fga.dev" 32 | FGA_API_TOKEN_ISSUER="auth.fga.dev" 33 | FGA_API_AUDIENCE="https://api.us1.fga.dev/" 34 | RESTRICTED_USER_ID_EXAMPLE="..." 35 | 36 | # SENTRY 37 | NEXT_PUBLIC_SENTRY_DSN="..." 38 | SENTRY_ORG="..." 39 | SENTRY_PROJECT="..." 40 | -------------------------------------------------------------------------------- /lib/market/stocks.ts: -------------------------------------------------------------------------------- 1 | import data from "./stocks.json"; 2 | 3 | export async function getStockPrices({ symbol }: { symbol: string }) { 4 | await new Promise((resolve) => setTimeout(resolve, 1000)); 5 | 6 | const result = data.find((stock) => stock.symbol.toLowerCase() === symbol.toLowerCase()); 7 | 8 | if (!result) { 9 | throw new Error(`Stock ${symbol} not found`); 10 | } 11 | 12 | return result; 13 | } 14 | 15 | export async function getCompanyInfo({ symbol }: { symbol: string }) { 16 | await new Promise((resolve) => setTimeout(resolve, 1000)); 17 | 18 | const result = data.find((stock) => stock.symbol.toLowerCase() === symbol.toLowerCase()); 19 | 20 | if (!result) { 21 | throw new Error(`Stock ${symbol} not found`); 22 | } 23 | 24 | return { 25 | symbol: result.symbol, 26 | shortname: result.shortname, 27 | longname: result.longname, 28 | industry: result.industry, 29 | sector: result.sector, 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /components/ui/toaster.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Toast, 5 | ToastClose, 6 | ToastDescription, 7 | ToastProvider, 8 | ToastTitle, 9 | ToastViewport, 10 | } from "@/components/ui/toast"; 11 | import { useToast } from "@/components/ui/use-toast"; 12 | 13 | export function Toaster() { 14 | const { toasts } = useToast(); 15 | 16 | return ( 17 | 18 | {toasts.map(function ({ id, title, description, action, ...props }) { 19 | return ( 20 | 21 |
22 | {title && {title}} 23 | {description && ( 24 | {description} 25 | )} 26 |
27 | {action} 28 | 29 |
30 | ); 31 | })} 32 | 33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /llm/actions/langchain-helpers.ts: -------------------------------------------------------------------------------- 1 | import { RELATION } from "@/lib/constants"; 2 | import { getDocumentsVectorStore } from "@/lib/documents"; 3 | import { fgaClient, getUser } from "@/sdk/fga"; 4 | import { FGARetriever } from "@/sdk/fga/langchain/rag"; 5 | 6 | export async function getSymbolRetriever(symbol: string) { 7 | const claims = await getUser(); 8 | 9 | // Get the db vector store 10 | const vectorStore = await getDocumentsVectorStore(); 11 | 12 | // Create a retriever that filters the documents by symbol 13 | const retriever = vectorStore.asRetriever({ 14 | filter: { symbol }, 15 | }); 16 | 17 | 18 | // Create a Retriever Wrapper that filters the documents by user access 19 | return new FGARetriever( 20 | { 21 | retriever, 22 | fgaClient, 23 | buildQuery: (doc) => ({ 24 | user: `user:${claims.sub}`, 25 | relation: RELATION.CAN_VIEW_DOCS, 26 | object: `doc:${doc.metadata.id}`, 27 | }) 28 | } 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Next.js: debug server-side", 6 | "type": "node-terminal", 7 | "request": "launch", 8 | "command": "npm run dev" 9 | }, 10 | { 11 | "name": "Next.js: debug client-side", 12 | "type": "chrome", 13 | "request": "launch", 14 | "url": "http://localhost:3000" 15 | }, 16 | { 17 | "name": "Next.js: debug full stack", 18 | "type": "node", 19 | "request": "launch", 20 | "program": "${workspaceFolder}/node_modules/.bin/next", 21 | "runtimeArgs": [ 22 | "--inspect" 23 | ], 24 | "skipFiles": [ 25 | "/**" 26 | ], 27 | "serverReadyAction": { 28 | "action": "debugWithEdge", 29 | "killOnServerStop": true, 30 | "pattern": "- Local:.+(https?://.+)", 31 | "uriFormat": "%s", 32 | "webRoot": "${workspaceFolder}" 33 | } 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /hooks/chat/use-copy-to-clipboard.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | /** 4 | * Copy to Clipboard Hook 5 | * 6 | * This is a simple react hook based on: 7 | * - https://github.com/vercel/ai-chatbot/blob/main/lib/hooks/use-copy-to-clipboard.tsx 8 | */ 9 | 10 | import * as React from "react"; 11 | 12 | export interface useCopyToClipboardProps { 13 | timeout?: number; 14 | } 15 | 16 | export function useCopyToClipboard({ 17 | timeout = 2000, 18 | }: useCopyToClipboardProps) { 19 | const [isCopied, setIsCopied] = React.useState(false); 20 | 21 | const copyToClipboard = async (value: string) => { 22 | if (typeof window === "undefined" || !navigator.clipboard?.writeText) { 23 | return; 24 | } 25 | 26 | if (!value) { 27 | return; 28 | } 29 | 30 | await navigator.clipboard.writeText(value); 31 | setIsCopied(true); 32 | 33 | setTimeout(() => { 34 | setIsCopied(false); 35 | }, timeout); 36 | }; 37 | 38 | return { isCopied, copyToClipboard }; 39 | } 40 | -------------------------------------------------------------------------------- /llm/tools/newsletter/check-subscription.tsx: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | import Loader from "@/components/loader"; 4 | import stocks from "@/lib/market/stocks.json"; 5 | import { checkEnrollment } from "@/llm/actions/newsletter"; 6 | import { defineTool } from "@/llm/ai-helpers"; 7 | import { withTextGeneration } from "@/llm/with-text-generation"; 8 | 9 | /** 10 | * This tool allows the user to check if they are subscribed to the newsletter. 11 | */ 12 | export default defineTool("check_subscription", async () => { 13 | return { 14 | description: `Check if the user is subscribed to the newsletter`, 15 | parameters: z.object({ 16 | }), 17 | generate: withTextGeneration({}, async function* () { 18 | yield ; 19 | const isUserEnrolled = await checkEnrollment({ symbol: stocks[0].symbol }); 20 | if (isUserEnrolled) { 21 | return 'You are subscribed to the newsletter'; 22 | } else { 23 | return 'You are not subscribed to the newsletter'; 24 | } 25 | }), 26 | }; 27 | }); 28 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | pg: 3 | container_name: pg 4 | image: pgvector/pgvector:pg16 5 | ports: 6 | - "${PGPORT}:5432" 7 | environment: 8 | - POSTGRES_USER=${PGUSER} 9 | - POSTGRES_PASSWORD=${PGPASSWORD} 10 | - POSTGRES_DB=${PGDATABASE} 11 | - TZ=${PGTZ:-UTC} 12 | - PGTZ=${PGTZ:-UTC} 13 | volumes: 14 | - pgdata:/var/lib/postgresql/data 15 | networks: 16 | - market0 17 | restart: always 18 | 19 | schemas: 20 | image: flyway/flyway 21 | command: migrate 22 | volumes: 23 | - ./database/market0/:/flyway/sql/ 24 | depends_on: 25 | - pg 26 | environment: 27 | - FLYWAY_LOCATIONS=filesystem:sql 28 | - FLYWAY_USER=${PGUSER} 29 | - FLYWAY_PASSWORD=${PGPASSWORD} 30 | - FLYWAY_CONNECT_RETRIES=60 31 | - FLYWAY_URL=jdbc:postgresql://pg/${PGDATABASE} 32 | - FLYWAY_BASELINE_ON_MIGRATE=true 33 | networks: 34 | - market0 35 | 36 | volumes: 37 | pgdata: 38 | 39 | networks: 40 | market0: 41 | driver: bridge 42 | -------------------------------------------------------------------------------- /sdk/components/connect-google-account.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { getGoogleConnectionName } from "@/lib/utils"; 4 | import { PromptUserContainer } from "@/llm/components/prompt-user-container"; 5 | 6 | export const ConnectGoogleAccount = ({ 7 | title, 8 | description, 9 | icon, 10 | action, 11 | api, 12 | readOnly = false, 13 | }: { 14 | title: string; 15 | description: string; 16 | icon?: React.ReactNode; 17 | action?: { label: string }; 18 | api: string; 19 | readOnly?: boolean; 20 | }) => { 21 | return ( 22 | <> 23 | { 30 | window.location.href = `/api/auth/login?3rdPartyApi=${api}&linkWith=${getGoogleConnectionName()}&returnTo=${ 31 | window.location.pathname 32 | }`; 33 | }, 34 | }} 35 | readOnly={readOnly} 36 | /> 37 | 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /hooks/chat/use-scroll-to-bottom.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Use scroll to bottom React hook 3 | * 4 | * This is a simple scroll hook based on: 5 | * - https://github.com/vercel/ai-chatbot/blob/2705d83d6cffdd8cde6679e5e6e4610e6eb71545/components/custom/use-scroll-to-bottom.ts 6 | */ 7 | import { RefObject, useEffect, useRef } from "react"; 8 | 9 | export function useScrollToBottom(): [RefObject, RefObject] { 10 | const containerRef = useRef(null); 11 | const endRef = useRef(null); 12 | 13 | useEffect(() => { 14 | const container = containerRef.current; 15 | const end = endRef.current; 16 | 17 | if (container && end) { 18 | const observer = new MutationObserver(() => { 19 | end.scrollIntoView({ behavior: "smooth", block: "end" }); 20 | }); 21 | 22 | observer.observe(container, { 23 | childList: true, 24 | subtree: true, 25 | attributes: true, 26 | characterData: true, 27 | }); 28 | 29 | return () => observer.disconnect(); 30 | } 31 | }, []); 32 | 33 | return [containerRef, endRef]; 34 | } 35 | -------------------------------------------------------------------------------- /components/explanation/observable.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, ReactNode, useContext, useState } from "react"; 2 | 3 | export enum ExplanationType { 4 | StocksUpcomingEvents = "stocks-upcoming-events", 5 | StockConditionalPurchase = "stock-conditional-purchase", 6 | Documents = "documents", 7 | } 8 | 9 | export type ExplanationMeta = { type: ExplanationType; expand?: boolean } | null; 10 | 11 | interface ObservableContextType { 12 | explanation: ExplanationMeta; 13 | setExplanation: React.Dispatch>; 14 | } 15 | 16 | const ObservableContext = createContext(undefined); 17 | 18 | export const ObservableProvider: React.FC<{ children: ReactNode }> = ({ children }) => { 19 | const [explanation, setExplanation] = useState(null); 20 | 21 | return {children}; 22 | }; 23 | 24 | export const useObservable = () => { 25 | const context = useContext(ObservableContext); 26 | if (!context) { 27 | throw new Error("useObservable must be used within an ObservableProvider"); 28 | } 29 | return context; 30 | }; 31 | -------------------------------------------------------------------------------- /llm/components/warning-wrapper.tsx: -------------------------------------------------------------------------------- 1 | import { ExplanationType } from "@/components/explanation/observable"; 2 | import { cn } from "@/lib/utils"; 3 | 4 | export default function WarningWrapper({ 5 | children, 6 | className, 7 | message, 8 | readOnly = false, 9 | explanationType, 10 | }: { 11 | children: React.ReactNode; 12 | className?: string; 13 | message?: React.ReactNode; 14 | readOnly?: boolean; 15 | explanationType?: ExplanationType; 16 | }) { 17 | return ( 18 |
*]:opacity-80 grayscale [&_*]:cursor-not-allowed": readOnly }, 23 | className 24 | )} 25 | disabled={readOnly} 26 | > 27 | {children} 28 |
29 | {message ? ( 30 | message 31 | ) : ( 32 | 33 | Data was randomly generated for illustrative purposes 34 | 35 | )} 36 |
37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /scripts/fga/syncDocs.ts: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | dotenv.config({ path: ".env.local" }); 3 | 4 | import { CredentialsMethod, OpenFgaClient } from "@openfga/sdk"; 5 | import { documents } from "../..//lib/db"; 6 | 7 | const fgaClient = new OpenFgaClient({ 8 | apiUrl: process.env.FGA_API_URL, 9 | storeId: process.env.FGA_STORE_ID, 10 | authorizationModelId: process.env.FGA_MODEL_ID, 11 | credentials: { 12 | method: CredentialsMethod.ClientCredentials, 13 | config: { 14 | apiTokenIssuer: process.env.FGA_API_TOKEN_ISSUER || "", 15 | apiAudience: process.env.FGA_API_AUDIENCE || "", 16 | clientId: process.env.FGA_CLIENT_ID || "", 17 | clientSecret: process.env.FGA_CLIENT_SECRET || "", 18 | }, 19 | }, 20 | }); 21 | 22 | (async function main() { 23 | console.log("Configuring earning reports tuples..."); 24 | 25 | const earningsReports = await documents.query("earning"); 26 | 27 | await fgaClient.write({ 28 | writes: [ 29 | ...earningsReports.map((report) => ({ 30 | user: "user:*", 31 | relation: "can_view", 32 | object: `doc:${report.metadata.id}`, 33 | })), 34 | ], 35 | }); 36 | 37 | process.exit(0); 38 | })(); 39 | -------------------------------------------------------------------------------- /lib/documents.ts: -------------------------------------------------------------------------------- 1 | import { NeonPostgres } from "@langchain/community/vectorstores/neon"; 2 | import { DistanceStrategy, PGVectorStore } from "@langchain/community/vectorstores/pgvector"; 3 | import { OpenAIEmbeddings } from "@langchain/openai"; 4 | 5 | const embeddings = new OpenAIEmbeddings({ 6 | model: "text-embedding-3-small", 7 | }); 8 | 9 | const docTableParams = { 10 | tableName: "documents", 11 | columns: { 12 | idColumnName: "id", 13 | vectorColumnName: "embedding", 14 | contentColumnName: "content", 15 | metadataColumnName: "metadata", 16 | }, 17 | }; 18 | 19 | export const getDocumentsVectorStore = async () => { 20 | if (process.env.USE_NEON) { 21 | const vectorStore = await NeonPostgres.initialize(embeddings, { 22 | connectionString: process.env.DATABASE_URL as string, 23 | ...docTableParams, 24 | }); 25 | return vectorStore; 26 | } 27 | 28 | return await PGVectorStore.initialize(embeddings, { 29 | postgresConnectionOptions: { 30 | connectionString: process.env.DATABASE_URL as string, 31 | }, 32 | ...docTableParams, 33 | // supported distance strategies: cosine (default), innerProduct, or euclidean 34 | distanceStrategy: "cosine", 35 | }); 36 | }; 37 | -------------------------------------------------------------------------------- /llm/components/forecasts/documents.mdx: -------------------------------------------------------------------------------- 1 | ## **Explained Prompt**: Show me forecast for ZEKO 2 | 3 | 4 | 5 | ### Scenario 6 | 7 | When a user submits a forecast inquiry for a specific company, like Zeko, the chatbot will generate a response using relevant documents retrieved from the vector store. By default, Market0 will only include publicly available filings. However, users may also have access to analyst-level forecasts, providing them with additional insights when the response is generated. 8 | 9 |
10 | Okta Fine Grained Authorization (FGA) is used to check which documents the user has access to based on their permissions. 11 | 12 | ### How it works 13 | 14 | 1. **User Forecast Request**: The user requests a forecast for a specific company, such as ZEKO. 15 | 2. **Document Retrieval**: Market0 handles the request and employs a retriever to search its vector store for documents relevant to the requested information. It applies filters to ensure only the documents the user has access to are considered. 16 | 3. **Response Generation**: Based on the retrieved documents, Market0 compiles a tailored response for the user. Depending on user's permissions the response could be based on analyst-level forecasts. -------------------------------------------------------------------------------- /llm/types.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | import * as serialization from "@/llm/components/serialization"; 4 | 5 | export { Document } from "@langchain/core/documents"; 6 | 7 | export interface ServerMessage { 8 | /** 9 | * Optional id for the message. 10 | * Make it easier to find the message in the UI and swap history. 11 | */ 12 | id?: string; 13 | 14 | role: "user" | "assistant" | "system" | "tool"; 15 | content: string | object; 16 | 17 | /** 18 | * The name of the component to render when recreating the UI from DB 19 | * 20 | * use serialization.names.get(ComponentName)! 21 | */ 22 | componentName?: keyof typeof serialization.components; 23 | 24 | /** 25 | * The parameters to pass to the component. 26 | */ 27 | params?: object; 28 | 29 | /** 30 | * If true, this message should not be shown in the UI. 31 | */ 32 | hidden?: boolean; 33 | } 34 | 35 | export interface Conversation { 36 | id: string; 37 | messages: ServerMessage[]; 38 | ownerID: string; 39 | title: string; 40 | createdAt: Date; 41 | updatedAt: Date; 42 | isPublic: boolean; 43 | } 44 | 45 | export interface ClientMessage { 46 | id: string; 47 | role: "user" | "assistant" | "function"; 48 | display: ReactNode; 49 | } 50 | -------------------------------------------------------------------------------- /components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const TooltipProvider = TooltipPrimitive.Provider 9 | 10 | const Tooltip = TooltipPrimitive.Root 11 | 12 | const TooltipTrigger = TooltipPrimitive.Trigger 13 | 14 | const TooltipContent = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef 17 | >(({ className, sideOffset = 4, ...props }, ref) => ( 18 | 27 | )) 28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName 29 | 30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } 31 | -------------------------------------------------------------------------------- /llm/utils.ts: -------------------------------------------------------------------------------- 1 | import { getMutableAIState } from "ai/rsc"; 2 | 3 | import { ServerMessage } from "./types"; 4 | 5 | /** 6 | * 7 | * This is a helper arround getMutableAIState to get 8 | * and update the history of the conversation. 9 | * 10 | * @returns 11 | */ 12 | export function getHistory() { 13 | const history = getMutableAIState(); 14 | 15 | return { 16 | get: (): ServerMessage[] => { 17 | return history.get(); 18 | }, 19 | update: (content: string | string[] | ServerMessage | ServerMessage[], role: string = "assistant") => { 20 | let newMessages; 21 | 22 | if (Array.isArray(content)) { 23 | newMessages = content.map((c) => { 24 | if (typeof c === "object") { 25 | return c; 26 | } 27 | return { 28 | role, 29 | content: c, 30 | }; 31 | }); 32 | } else if (typeof content === "object") { 33 | newMessages = [content]; 34 | } else { 35 | newMessages = [ 36 | { 37 | role, 38 | content, 39 | }, 40 | ]; 41 | } 42 | 43 | history.done((messages: ServerMessage[]) => [ 44 | ...messages, 45 | ...newMessages, 46 | ]); 47 | }, 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /llm/components/stocks/conditional-purchase.mdx: -------------------------------------------------------------------------------- 1 | ## **Explained Prompt**: Buy ZEKO if P/E ratio is above 15 2 | 3 | 4 | 5 | ### Scenario 6 | 7 | When the user requests to buy a specific company stock when its P/E ratio is above 15, the chatbot will monitor this metric and seek the user's approval (authorization) before executing any transaction. 8 | 9 |
10 | This process is streamlined through Auth0, utilizing the CIBA (client-initiated backchannel authentication) flow to facilitate 11 | asynchronous user approval as needed. This ensures the user consents to the operation before any purchase order is executed. 12 | 13 | ### How it works 14 | 15 | 1. **User Initiates Purchase Request**: The user instructs the chatbot to monitor a specific company's stock, indicating a desire to purchase if the P/E ratio exceeds zero. 16 | 2. **Monitoring and Notification**: Market0 continuously tracks the company's P/E ratio. When it rises above zero, the system notifies the user (using CIBA) to approve the purchase. 17 | 3. **User Confirmation**: Upon notification, the user reviews the details and confirms whether to proceed with the transaction. 18 | 4. **Purchase Completion**: Once the user approves, Market0 completes the purchase of 10 shares of the specified company. -------------------------------------------------------------------------------- /llm/components/stocks/stock-skeleton.tsx: -------------------------------------------------------------------------------- 1 | import WarningWrapper from "../warning-wrapper"; 2 | 3 | export const StockSkeleton = () => { 4 | return ( 5 | 6 |
7 |
8 |
9 |
xxxxx
10 |
11 | xxxxxx xxx xx xxxx xx xxx 12 |
13 |
14 |
15 |
16 | xxx 17 |
18 |
xxxxxx xxx xx xxxx
19 |
20 |
21 | 22 |
23 |
24 |
25 |
26 |
27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ) 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /llm/components/events/events.mdx: -------------------------------------------------------------------------------- 1 | ## **Explained Prompt**: Show me ZEKO upcoming events 2 | 3 | 4 | 5 | ### Scenario 6 | 7 | When a user requests information about upcoming events for a company, such as ZEKO, the chatbot will retrieve and display a list of relevant events. Additionally, it will offer to check the user's availability in their Google Calendar to add reminders for selected events. 8 | 9 |
10 | To check availability, integration with the Google Calendar API is necessary, with Auth0 handling user authentication and 11 | authorization for seamless access. 12 | 13 | ### How it works 14 | 15 | 1. **Users Requests Upcoming Events**: The user requests upcoming events for a specific company such as ZEKO. 16 | 2. **Displaying Events and Availability Check**: Market0 handles the user's request and displays the list of upcoming events. As part of the response, it offers an option for the user to check their availability in Google Calendar. 17 | 3. **Third-Party Service Authorization**: When the user clicks the "Check" button, Auth0 will handle the user's authorization to call the Google Calendar API on their behalf. 18 | 4. **API Call on Behalf of User**: Once authorized, the app checks the user’s Google Calendar availability, allowing users to add event reminders to their schedule. 19 | -------------------------------------------------------------------------------- /components/chat/header.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import { Auth0Icon, IconAuth0 } from "@/components/icons"; 4 | import { getSession } from "@auth0/nextjs-auth0"; 5 | 6 | import { Navbar } from "./navbar"; 7 | 8 | export async function Header({ 9 | children, 10 | outerElements, 11 | allowLogin = true, 12 | leftElements, 13 | }: { 14 | children?: React.ReactNode; 15 | outerElements?: React.ReactNode; 16 | leftElements?: React.ReactNode; 17 | allowLogin?: boolean; 18 | }) { 19 | const session = await getSession(); 20 | const user = session?.user; 21 | 22 | return ( 23 |
24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | {leftElements} 32 |
33 | 34 | 35 | {children} 36 | 37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /lib/db/transactions.ts: -------------------------------------------------------------------------------- 1 | import { sql } from "./sql"; 2 | 3 | type Transaction = { 4 | id: number; 5 | ticker_id: string; 6 | type: "buy" | "sell"; 7 | price: number; 8 | quantity: number; 9 | created_at: Date; 10 | updated_at: Date; 11 | }; 12 | 13 | export const create = async ( 14 | ticker_id: string, 15 | price: number, 16 | quantity: number, 17 | type: "buy" | "sell", 18 | user_id: string 19 | ): Promise => { 20 | const result = await sql` 21 | INSERT INTO transactions (ticker_id, price, quantity, type, user_id) 22 | VALUES (${ticker_id}, ${price}, ${quantity}, ${type}, ${user_id}) 23 | RETURNING * 24 | `; 25 | 26 | return result[0] as Transaction; 27 | }; 28 | 29 | export type Position = { 30 | ticker_id: string; 31 | quantity: number; 32 | av_price: number; 33 | }; 34 | 35 | export const getPositions = async (user_id: string): Promise => { 36 | const result = await sql ` 37 | SELECT 38 | ticker_id, 39 | SUM(CASE WHEN type = 'buy' THEN quantity ELSE -quantity END) as quantity, 40 | AVG(CASE WHEN type = 'buy' THEN price END) as av_price 41 | FROM transactions 42 | WHERE user_id = ${user_id} 43 | GROUP BY ticker_id 44 | HAVING SUM(CASE WHEN type = 'buy' THEN quantity ELSE -quantity END) > 0 45 | `; 46 | 47 | return result as unknown as Position[]; 48 | }; 49 | -------------------------------------------------------------------------------- /components/chat/context.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { GetUsers200ResponseOneOfInner } from "auth0"; 4 | import React, { createContext, ReactNode, useContext, useState } from "react"; 5 | 6 | interface ChatContextProps { 7 | chatId?: string; 8 | readOnly: boolean; 9 | hasMessages: boolean; 10 | ownerProfile?: GetUsers200ResponseOneOfInner; 11 | setChatId: (id?: string) => void; 12 | setHasMessages: (has: boolean) => void; 13 | } 14 | 15 | const ChatContext = createContext(undefined); 16 | 17 | export const ChatProvider = ({ 18 | chatId: initialChatId, 19 | hasMessages: initialHasMessages = false, 20 | readOnly = true, 21 | ownerProfile, 22 | children, 23 | }: { 24 | chatId?: string; 25 | hasMessages?: boolean; 26 | readOnly?: boolean; 27 | ownerProfile?: GetUsers200ResponseOneOfInner; 28 | children: ReactNode; 29 | }) => { 30 | const [chatId, setChatId] = useState(initialChatId); 31 | const [hasMessages, setHasMessages] = useState(initialHasMessages); 32 | 33 | return ( 34 | 35 | {children} 36 | 37 | ); 38 | }; 39 | 40 | export const useChat = () => { 41 | const context = useContext(ChatContext); 42 | if (!context) { 43 | throw new Error("useChat must be used within a ChatProvider"); 44 | } 45 | return context; 46 | }; 47 | -------------------------------------------------------------------------------- /llm/tools/profile/set-profile-attributes.tsx: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | import Loader from "@/components/loader"; 4 | import { defineTool } from "@/llm/ai-helpers"; 5 | import { withTextGeneration } from "@/llm/with-text-generation"; 6 | import { updateUser } from "@/sdk/auth0/mgmt"; 7 | import { getUser } from "@/sdk/fga"; 8 | 9 | /** 10 | * This tool allows the user to set their first name and/or last name. 11 | * The changes are propagated to the Auth0's user profile. 12 | */ 13 | export default defineTool("set_profile_attributes", async () => { 14 | return { 15 | description: `Allows the user to set their first name and/or last name. 16 | `, 17 | parameters: z.object({ 18 | givenName: z.string().optional().describe("The first name of the user"), 19 | familyName: z.string().optional().describe("The last name of the user"), 20 | }), 21 | generate: withTextGeneration( 22 | {}, 23 | async function* ({ givenName, familyName }: { givenName?: string; familyName?: string }) { 24 | yield ; 25 | 26 | const user = await getUser(); 27 | await updateUser(user.sub, { givenName, familyName }); 28 | 29 | return ` 30 | Your profile has been updated successfully. 31 | ${givenName ? `Your first name is now ${givenName}.` : ""} 32 | ${familyName ? `Your last name is now ${familyName}.` : ""} 33 | `; 34 | } 35 | ), 36 | }; 37 | }); 38 | -------------------------------------------------------------------------------- /components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as PopoverPrimitive from "@radix-ui/react-popover" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Popover = PopoverPrimitive.Root 9 | 10 | const PopoverTrigger = PopoverPrimitive.Trigger 11 | 12 | const PopoverAnchor = PopoverPrimitive.Anchor 13 | 14 | const PopoverContent = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef 17 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 18 | 19 | 29 | 30 | )) 31 | PopoverContent.displayName = PopoverPrimitive.Content.displayName 32 | 33 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } 34 | -------------------------------------------------------------------------------- /lib/db/documents.ts: -------------------------------------------------------------------------------- 1 | import { getUser } from "@/sdk/fga"; 2 | 3 | import { sql } from "./sql"; 4 | 5 | export type Document = { 6 | id: number; 7 | metadata: { 8 | id: string; 9 | title: string; 10 | symbol: string; 11 | link: string; 12 | type: 'earning' | 'forecast'; 13 | }; 14 | content: string; 15 | }; 16 | 17 | export const getLatestEarningReport = async (symbol: string) => { 18 | const res = await sql` 19 | SELECT * 20 | FROM documents 21 | WHERE metadata->>'type' = 'earning' 22 | AND metadata->>'symbol' = ${symbol} 23 | ORDER BY id DESC LIMIT 1 24 | `; 25 | return res[0]; 26 | }; 27 | 28 | export const query = async (type?: 'earning' | 'forecast', symbol?: string): Promise => { 29 | const res = await sql` 30 | SELECT * 31 | FROM documents 32 | WHERE (metadata->>'type' = ${type ?? null} OR ${type == null}) 33 | AND (metadata->>'symbol' = ${symbol ?? null} OR ${symbol == null}) 34 | `; 35 | return res; 36 | }; 37 | 38 | export const getByID = async (id: string): Promise => { 39 | const res = await sql` 40 | SELECT * 41 | FROM documents 42 | WHERE metadata->>'id' = ${id} 43 | `; 44 | return res[0]; 45 | }; 46 | 47 | export const removeAll = async (docType: "earning" | "forecast"): Promise => { 48 | await sql` 49 | DELETE FROM documents 50 | WHERE metadata->>'type' = ${docType} 51 | `; 52 | }; 53 | -------------------------------------------------------------------------------- /llm/tools/trading/show-current-positions.tsx: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | import { transactions } from "@/lib/db"; 4 | import { defineTool } from "@/llm/ai-helpers"; 5 | import * as serialization from "@/llm/components/serialization"; 6 | import { Positions } from "@/llm/components/stocks"; 7 | import { getHistory } from "@/llm/utils"; 8 | import { withTextGeneration } from "@/llm/with-text-generation"; 9 | import { getUser } from "@/sdk/fga"; 10 | 11 | /** 12 | * This tool allow the user to check their current stock positions. 13 | */ 14 | export default defineTool("show_current_positions", () => { 15 | const history = getHistory(); 16 | return { 17 | description: 18 | "Show the current stock positions the user has. Use this tool when the user request to see their current stock positions or holdings.", 19 | parameters: z.object({}), 20 | generate: withTextGeneration(async function* () { 21 | const user = await getUser(); 22 | const positions = await transactions.getPositions(user.sub); 23 | 24 | if (positions.length === 0) { 25 | return "You do not have any stock positions yet."; 26 | } 27 | 28 | history.update({ 29 | role: "assistant", 30 | content: `[Current User Positions ${JSON.stringify(positions)}]`, 31 | componentName: serialization.names.get(Positions)!, 32 | params: { positions }, 33 | }); 34 | 35 | return ; 36 | }), 37 | }; 38 | }); 39 | -------------------------------------------------------------------------------- /lib/examples.tsx: -------------------------------------------------------------------------------- 1 | import { generateId } from "ai"; 2 | 3 | import { 4 | CartIcon, 5 | EarningsIcon, 6 | PurchaseStockIcon, 7 | ReminderIcon, 8 | ShowEventsIcon, 9 | StockPriceIcon, 10 | TrendingStocksIcon, 11 | } from "@/components/icons"; 12 | 13 | export const examples = [ 14 | { 15 | id: generateId(), 16 | title: "Show me ZEKO upcoming events", 17 | message: "Show me ZEKO upcoming events", 18 | }, 19 | { 20 | id: generateId(), 21 | title: "Show me forecast for ZEKO", 22 | message: "Show me forecast for ZEKO", 23 | }, 24 | { 25 | id: generateId(), 26 | title: "Buy ZEKO if P/E ratio is above 15", 27 | message: "Buy 10 ZEKO when P/E ratio is above 15", 28 | }, 29 | ]; 30 | 31 | export const menuItems = [ 32 | { 33 | id: generateId(), 34 | message: "What's the stock price of ZEKO?", 35 | icon: , 36 | }, 37 | { 38 | id: generateId(), 39 | message: "Show me earnings for ZEKO", 40 | icon: , 41 | }, 42 | { 43 | id: generateId(), 44 | message: "Show me forecast for ZEKO", 45 | icon: , 46 | }, 47 | { 48 | id: generateId(), 49 | message: "Show me ZEKO upcoming events", 50 | icon: , 51 | }, 52 | { 53 | id: generateId(), 54 | message: "Buy 10 shares of ZEKO", 55 | icon: , 56 | }, 57 | { 58 | id: generateId(), 59 | message: "Buy 10 ZEKO when P/E ratio is above 15", 60 | icon: , 61 | }, 62 | ]; 63 | -------------------------------------------------------------------------------- /sdk/auth0/3rd-party-apis/providers/box.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { getSession } from "@auth0/nextjs-auth0"; 4 | 5 | const fetchAccessToken = async (auth0IdToken: string): Promise => { 6 | const res = await fetch(`${process.env.AUTH0_ISSUER_BASE_URL}/oauth/token`, { 7 | method: "POST", 8 | headers: { "Content-Type": "application/json" }, 9 | body: JSON.stringify({ 10 | grant_type: "urn:ietf:params:oauth:grant-type:token-exchange", 11 | client_id: process.env.AUTH0_CLIENT_ID, 12 | client_secret: process.env.AUTH0_CLIENT_SECRET, 13 | subject_token_type: "urn:ietf:params:oauth:token-type:id_token", 14 | subject_token: auth0IdToken, 15 | requested_token_type: "http://auth0.com/oauth/token-type/social-access-token/box", 16 | }), 17 | }); 18 | 19 | if (!res.ok) { 20 | throw new Error(`Unable to get a Box API access token: ${await res.text()}`); 21 | } 22 | 23 | const { access_token } = await res.json(); 24 | return access_token; 25 | }; 26 | 27 | export async function getAccessToken() { 28 | "use server"; 29 | const session = await getSession(); 30 | const auth0IdToken = session?.idToken!; 31 | let accessToken = undefined; 32 | 33 | try { 34 | accessToken = await fetchAccessToken(auth0IdToken); 35 | return accessToken; 36 | } catch (e) {} 37 | 38 | return accessToken; 39 | } 40 | 41 | export async function withBoxApi(fn: (accessToken: string) => Promise) { 42 | const accessToken = await getAccessToken(); 43 | return await fn(accessToken!); 44 | } 45 | -------------------------------------------------------------------------------- /app/report/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import Markdown from "react-markdown"; 4 | 5 | import { ErrorContainer } from "@/components/fga/error"; 6 | import { documents } from "@/lib/db"; 7 | import { withFGA } from "@/sdk/fga"; 8 | import { withCheckPermission } from "@/sdk/fga/next/with-check-permission"; 9 | 10 | type ReportParams = { 11 | params: { 12 | id: string; 13 | }; 14 | }; 15 | 16 | async function Report({ params }: ReportParams) { 17 | const document = await documents.getByID(params.id); 18 | return ( 19 |
20 |
21 |
22 |
23 |
24 |

{document.metadata.title}

25 | {document.content} 26 |
27 |
28 |
29 |
30 | ); 31 | } 32 | 33 | export default withCheckPermission( 34 | { 35 | checker: ({ params }: ReportParams) => 36 | withFGA({ 37 | object: `doc:${params.id}`, 38 | relation: "can_view", 39 | }), 40 | onUnauthorized: async () => { 41 | return ; 42 | }, 43 | }, 44 | Report 45 | ); 46 | -------------------------------------------------------------------------------- /llm/tools/trading/list-stocks.tsx: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | import allStocks from "@/lib/market/stocks.json"; 4 | import { defineTool } from "@/llm/ai-helpers"; 5 | import * as serialization from "@/llm/components/serialization"; 6 | import { Stocks, StocksSkeleton } from "@/llm/components/stocks"; 7 | import { getHistory } from "@/llm/utils"; 8 | 9 | /** 10 | * This tool allow the user to retrieve a list of stocks. 11 | */ 12 | export default defineTool("list_stocks", () => { 13 | const history = getHistory(); 14 | 15 | return { 16 | description: "List stocks, always list ATKO.", 17 | parameters: z.object({ 18 | tickers: z.array(z.string().describe("The symbol or ticker of the stock")), 19 | }), 20 | generate: async function* ({ tickers }) { 21 | yield ; 22 | 23 | await new Promise((resolve) => setTimeout(resolve, 1000)); 24 | 25 | const stocks = tickers.map((ticker) => { 26 | const stock = allStocks.find((stock) => stock.symbol === ticker); 27 | return { 28 | symbol: stock?.symbol, 29 | price: stock?.current_price, 30 | delta: stock?.delta, 31 | market: stock?.exchange, 32 | company: stock?.shortname, 33 | currency: "USD", 34 | }; 35 | }); 36 | 37 | history.update({ 38 | role: "assistant", 39 | componentName: serialization.names.get(Stocks), 40 | params: { stocks }, 41 | content: `[Listed stocks: ${JSON.stringify({ stocks })}]`, 42 | }); 43 | 44 | return ; 45 | }, 46 | }; 47 | }); 48 | -------------------------------------------------------------------------------- /components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | Avatar.displayName = AvatarPrimitive.Root.displayName 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )) 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 49 | 50 | export { Avatar, AvatarImage, AvatarFallback } 51 | -------------------------------------------------------------------------------- /llm/tools/trading/show-stock-price.tsx: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | import { getStockPrices } from "@/lib/market/stocks"; 4 | import { defineTool } from "@/llm/ai-helpers"; 5 | import * as serialization from "@/llm/components/serialization"; 6 | import { Stock, StockSkeleton } from "@/llm/components/stocks"; 7 | import { getHistory } from "@/llm/utils"; 8 | 9 | /** 10 | * This tool allow the user to check the price of a stock. 11 | */ 12 | export default defineTool("show_stock_price", () => { 13 | const history = getHistory(); 14 | 15 | return { 16 | description: "Check price of an stock. Prefer this when there is not clear intention of buying.", 17 | parameters: z.object({ 18 | symbol: z.string().describe("The name or symbol of the stock. e.g. DOGE/AAPL/USD."), 19 | }), 20 | generate: async function* ({ symbol }) { 21 | yield ; 22 | 23 | const doc = await getStockPrices({ symbol }); 24 | 25 | if (!doc) { 26 | history.update(`[Price not found for ${symbol}]`); 27 | return <>Price not found; 28 | } 29 | 30 | const { delta, current_price: price } = doc; 31 | 32 | const params = { 33 | symbol, 34 | price, 35 | delta, 36 | company: doc.shortname, 37 | currency: "USD", 38 | market: doc.exchange, 39 | }; 40 | 41 | history.update({ 42 | role: "assistant", 43 | content: `[Price of ${symbol} = ${price}]`, 44 | componentName: serialization.names.get(Stock)!, 45 | params, 46 | }); 47 | 48 | return ; 49 | }, 50 | }; 51 | }); 52 | -------------------------------------------------------------------------------- /llm/tools/newsletter/set-subscription.tsx: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | import Loader from "@/components/loader"; 4 | import stocks from "@/lib/market/stocks.json"; 5 | import { checkEnrollment, enrollToNewsletter, unenrollFromNewsletter } from "@/llm/actions/newsletter"; 6 | import { defineTool } from "@/llm/ai-helpers"; 7 | import { withTextGeneration } from "@/llm/with-text-generation"; 8 | 9 | /** 10 | * This tool allows the user to subscribe or unsubscribe to the newsletter. 11 | */ 12 | export default defineTool("set_subscription", async () => { 13 | return { 14 | description: `Allows the user to subscribe or unsubscribe to newsletter. 15 | `, 16 | parameters: z.object({ 17 | action: z.enum(['subscribe', 'unsubscribe']) 18 | }), 19 | generate: withTextGeneration(async function* ({ action }: { action: 'subscribe' | 'unsubscribe' }) { 20 | yield ; 21 | 22 | const isUserEnrolled = await checkEnrollment({ symbol: stocks[0].symbol }); 23 | if (action === 'subscribe') { 24 | if (isUserEnrolled) { 25 | return 'You are already subscribed to newsletter.'; 26 | } 27 | await enrollToNewsletter(); 28 | return `The user has successfully subscribed to the newsletter. 29 | Be sure to thank them for subscribing. 30 | Optionally, offer them a forecast analysis. 31 | `; 32 | } else { 33 | if (!isUserEnrolled) { 34 | return 'You are not subscribed to newsletter.'; 35 | } 36 | await unenrollFromNewsletter(); 37 | } 38 | return 'Subscription updated successfully.'; 39 | }), 40 | }; 41 | }); 42 | -------------------------------------------------------------------------------- /llm/system-prompt.ts: -------------------------------------------------------------------------------- 1 | import { getUser } from "@/sdk/fga"; 2 | 3 | import stocks from "../lib/market/stocks.json"; 4 | 5 | const llmUserAttributes = ['email', 'name', 'given_name', 'family_name', 'nickname', 'picture']; 6 | 7 | export async function getSystemPrompt() { 8 | const user = await getUser(); 9 | const userData = Object.fromEntries( 10 | Object.entries(user).filter(([key]) => llmUserAttributes.includes(key)) 11 | ); 12 | return ` 13 | You are a specialized stock trading assistant designed to guide users through the process of buying stocks step by step. 14 | 15 | **Market Scope**: 16 | Your available market consists of only ${stocks.length} stocks. Here are the details of each: 17 | 18 | ${stocks.map((stock) => `- **Ticker**: ${stock.symbol} 19 | **Name**: ${stock.longname} 20 | **Summary**: ${stock.long_business_summary}`).join("\n")} 21 | 22 | **Important Constraints**: 23 | - You cannot discuss, buy, or sell any stocks outside this limited list, whether real or fictional. 24 | - You and the user can discuss the prices of these stocks, adjust stock amounts, and place buy orders through the UI. 25 | 26 | **User Interactions**: 27 | Messages in brackets ([]) indicate either a UI element or a user-triggered action. For example: 28 | - "[Price of AAPL = 100]" displays the price of AAPL stock to the user. 29 | - "[User has changed the amount of AAPL to 10]" indicates the user updated the amount of AAPL to 10. 30 | 31 | **Additional Guidelines**: 32 | - Today’s date for reference: ${new Date()} 33 | - User data: ${JSON.stringify(userData)} 34 | - You may perform calculations as needed and engage in general discussion with the user.`; 35 | }; 36 | -------------------------------------------------------------------------------- /hooks/auth0/helpers/rate-limit.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Rate limiting 3 | * 4 | * This is a simple rate limiting based on: 5 | * - https://github.com/vercel/next.js/tree/main/examples/api-routes-rate-limit 6 | */ 7 | import { LRUCache } from "lru-cache"; 8 | import { NextResponse } from "next/server"; 9 | 10 | type Options = { 11 | uniqueTokenPerInterval?: number; 12 | interval?: number; 13 | }; 14 | 15 | function rateLimit(options?: Options) { 16 | const tokenCache = new LRUCache({ 17 | max: options?.uniqueTokenPerInterval || 500, 18 | ttl: options?.interval || 60000, 19 | }); 20 | 21 | return { 22 | check: (limit: number, token: string) => 23 | new Promise((resolve, reject) => { 24 | const tokenCount = (tokenCache.get(token) as number[]) || [0]; 25 | if (tokenCount[0] === 0) { 26 | tokenCache.set(token, tokenCount); 27 | } 28 | tokenCount[0] += 1; 29 | 30 | const currentUsage = tokenCount[0]; 31 | const isRateLimited = currentUsage >= limit; 32 | 33 | return isRateLimited ? reject() : resolve(); 34 | }), 35 | }; 36 | } 37 | 38 | const limiter = rateLimit({ 39 | interval: 2 * 1000, 40 | uniqueTokenPerInterval: 500, 41 | }); 42 | 43 | export function withRateLimit(handler: any) { 44 | return async (req: Request, res: Response) => { 45 | try { 46 | // Note: Update the limit and token as needed 47 | await limiter.check(10, process.env.LRU_CACHE_TOKEN!); 48 | return await handler(req, res); 49 | } catch (error) { 50 | return NextResponse.json( 51 | { error: "Rate limit exceeded" }, 52 | { status: 429 } 53 | ); 54 | } 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /llm/tools/profile/set-employeer.tsx: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | import Loader from "@/components/loader"; 4 | import { defineTool } from "@/llm/ai-helpers"; 5 | import { withTextGeneration } from "@/llm/with-text-generation"; 6 | import { fgaClient, getUser } from "@/sdk/fga"; 7 | 8 | /** 9 | * This tool allows the user to set or unset its current employeer. 10 | */ 11 | export default defineTool("set_employeer", async () => { 12 | return { 13 | description: `Allows the user to set or unset its current employeer. 14 | Use this tool when the user purposedly set or casually mention its current employeer. 15 | `, 16 | parameters: z.object({ 17 | employeer: z.string().describe("The ticker of the company the user is currently working for"), 18 | status: z.boolean().describe("Whether the user is currently working for the company"), 19 | }), 20 | generate: withTextGeneration({}, async function* ({ employeer, status }: { employeer: string; status: boolean }) { 21 | yield ; 22 | 23 | const user = await getUser(); 24 | 25 | try { 26 | await fgaClient.write({ 27 | [status ? "writes" : "deletes"]: [ 28 | { 29 | user: `user:${user.sub}`, 30 | relation: "employee", 31 | object: "company:atko", 32 | }, 33 | ], 34 | }); 35 | } catch (err: any) { 36 | // it might fail because the tuple does not exist 37 | // or already exist. we don't care about that. 38 | console.warn(err?.message); 39 | } 40 | 41 | return `Noted that you are ${status ? "now" : "no longer"} working for ${employeer}.`; 42 | }), 43 | }; 44 | }); 45 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "editor.formatOnSave": true, 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll.tslint": "explicit" 6 | }, 7 | "editor.defaultFormatter": "esbenp.prettier-vscode", 8 | "eslint.format.enable": true, 9 | "importSorter.importStringConfiguration.trailingComma": "multiLine", 10 | "importSorter.generalConfiguration.sortOnBeforeSave": true, 11 | "importSorter.importStringConfiguration.quoteMark": "double", 12 | "importSorter.importStringConfiguration.maximumNumberOfImportExpressionsPerLine.count": 120, 13 | "importSorter.importStringConfiguration.tabSize": 2, 14 | "importSorter.importStringConfiguration.maximumNumberOfImportExpressionsPerLine.type": "newLineEachExpressionAfterCountLimitExceptIfOnlyOne", 15 | "importSorter.sortConfiguration.customOrderingRules.rules": [ 16 | { 17 | "type": "importMember", 18 | "regex": "^$", 19 | "orderLevel": 5, 20 | "disableSort": true 21 | }, 22 | { 23 | "regex": "^[^.#@]", 24 | "orderLevel": 10 25 | }, 26 | { 27 | "regex": "^[@]", 28 | "orderLevel": 15 29 | }, 30 | { 31 | "regex": "^#[.]", 32 | "orderLevel": 20 33 | }, 34 | { 35 | "regex": "^[.]", 36 | "orderLevel": 30 37 | } 38 | ], 39 | "importSorter.generalConfiguration.exclude": ["./scripts"], 40 | "sql-formatter.dialect": "pl/sql", 41 | "[sql]": { 42 | "editor.defaultFormatter": "bradymholt.pgformatter" 43 | }, 44 | "[typescriptreact]": { 45 | "editor.defaultFormatter": "esbenp.prettier-vscode" 46 | }, 47 | "[mdx]": { 48 | "editor.formatOnSave": false, 49 | "editor.formatOnPaste": false, 50 | "editor.formatOnType": false 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /sdk/fga/next/with-check-permission.tsx: -------------------------------------------------------------------------------- 1 | type WithCheckPermissionParams = { 2 | checker: (...args: Args) => Promise; 3 | onUnauthorized?: (...args: Args) => any; 4 | }; 5 | 6 | const DEFAULT_UNAUTHORIZED_MESSAGE = 7 | "You are not authorized to perform this action."; 8 | 9 | async function defaultOnUnauthorized(): Promise { 10 | return
{DEFAULT_UNAUTHORIZED_MESSAGE}
; 11 | } 12 | 13 | /** 14 | * Wrap a Serverless page or action with an FGA permission check. 15 | * 16 | * @param options 17 | * @param params.checker - A function that receives the same parameters 18 | * as the serverless function and checks 19 | * if the user has permission to access the page. 20 | * This can be `withFGA`. 21 | * @param params.onUnauthorized - A function that receives the same parameters 22 | * as the serverless function and returns a JSX element. 23 | * This can be used to customize the unauthorized message. 24 | * @param fn - The serverless function to wrap. 25 | * @returns A new serverless function that checks permissions before executing the original function. 26 | */ 27 | export function withCheckPermission Promise, Args extends Parameters>( 28 | options: WithCheckPermissionParams, 29 | fn: F 30 | ): F { 31 | return async function (...args: Args): Promise { 32 | let allowed = false; 33 | 34 | try { 35 | allowed = await options.checker(...args); 36 | } catch (e) { 37 | console.error(e); 38 | } 39 | 40 | if (allowed) { 41 | return fn(...args); 42 | } else { 43 | if (options.onUnauthorized) { 44 | return options.onUnauthorized(...args); 45 | } 46 | 47 | return defaultOnUnauthorized() as R; 48 | } 49 | } as F; 50 | } 51 | -------------------------------------------------------------------------------- /components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const ScrollArea = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, children, ...props }, ref) => ( 12 | 17 | 18 | {children} 19 | 20 | 21 | 22 | 23 | )) 24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName 25 | 26 | const ScrollBar = React.forwardRef< 27 | React.ElementRef, 28 | React.ComponentPropsWithoutRef 29 | >(({ className, orientation = "vertical", ...props }, ref) => ( 30 | 43 | 44 | 45 | )) 46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName 47 | 48 | export { ScrollArea, ScrollBar } 49 | -------------------------------------------------------------------------------- /lib/db/userUsage.ts: -------------------------------------------------------------------------------- 1 | import { LanguageModelUsage } from "ai"; 2 | 3 | import { sql } from "./sql"; 4 | 5 | export const track = async ( 6 | userID: string, 7 | usage: number | LanguageModelUsage | Promise 8 | ) => { 9 | let tokens = 0; 10 | if (typeof usage === "number") { 11 | tokens = usage; 12 | } else { 13 | tokens = (await usage).totalTokens; 14 | } 15 | await sql` 16 | INSERT INTO token_usage (user_id, tokens_used) 17 | VALUES (${userID}, ${tokens}); 18 | `; 19 | 20 | return getUsage(userID); 21 | } 22 | 23 | export const getUsage = async (userID: string) => { 24 | const result = await sql` 25 | SELECT 26 | SUM(tokens_used) FILTER (WHERE timestamp > NOW() - INTERVAL '1 hour') AS hour, 27 | SUM(tokens_used) FILTER (WHERE timestamp > NOW() - INTERVAL '1 day') AS day 28 | FROM token_usage 29 | WHERE user_id = ${userID} 30 | `; 31 | 32 | return { 33 | lastHour: result[0].hour, 34 | lastDay: result[0].day 35 | }; 36 | }; 37 | 38 | const dailyLimit = process.env.DAILY_TOKEN_LIMIT ? parseInt(process.env.DAILY_TOKEN_LIMIT, 10) : Infinity; 39 | const hourlyLimit = process.env.HOURLY_TOKEN_LIMIT ? parseInt(process.env.HOURLY_TOKEN_LIMIT, 10) : Infinity; 40 | const unthrottledUsers = process.env.UNTHROTTLED_USERS ? process.env.UNTHROTTLED_USERS.split(",") : []; 41 | const unlimitedUsers = process.env.UNLIMITED_USERS ? process.env.UNLIMITED_USERS.split(",") : []; 42 | 43 | export const hasAvailableTokens = async (userID: string, email: string): Promise => { 44 | if (unlimitedUsers.includes(email) || unlimitedUsers.includes(userID)) { 45 | return true; 46 | } 47 | const stats = await getUsage(userID); 48 | if (unthrottledUsers.includes(userID)) { 49 | return true; 50 | } 51 | return stats.lastDay <= dailyLimit && 52 | stats.lastHour <= hourlyLimit; 53 | } 54 | -------------------------------------------------------------------------------- /llm/components/stocks/positions.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useActions, useUIState } from "ai/rsc"; 4 | 5 | import { transactions } from "@/lib/db"; 6 | import { cn } from "@/lib/utils"; 7 | import { ClientMessage } from "@/llm/types"; 8 | 9 | import WarningWrapper from "../warning-wrapper"; 10 | 11 | export function Positions({ positions, readOnly = false }: { positions: transactions.Position[]; readOnly?: boolean }) { 12 | const [, setMessages] = useUIState(); 13 | const { continueConversation } = useActions(); 14 | 15 | return ( 16 | 17 |
18 | {positions.map((position) => ( 19 | 40 | ))} 41 |
42 |
43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /components/auth0/basic-info-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; 4 | import { Input } from "@/components/ui/input"; 5 | import { Label } from "@/components/ui/label"; 6 | 7 | interface KeyValueMap { 8 | [key: string]: any; 9 | } 10 | 11 | export default function BasicInfoForm({ user }: { user: KeyValueMap }) { 12 | const name = user.name; 13 | const email = user.email; 14 | const nickname = user.nickname; 15 | const phone = user.phone_number; 16 | 17 | return ( 18 | 19 | 20 | General 21 | Read only profile 22 | 23 | 24 |
25 |
26 | 27 | 28 |
29 |
30 | 31 | 32 |
33 |
34 | 35 | 36 |
37 |
38 | 39 | 40 |
41 |
42 |
43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /llm/components/prompt-user-container.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | export interface PromptUserContainerProps { 4 | title: React.ReactNode; 5 | description?: React.ReactNode; 6 | action?: { 7 | label: string; 8 | onClick: () => void; 9 | className?: string; 10 | }; 11 | icon?: React.ReactNode; 12 | readOnly?: boolean; 13 | containerClassName?: string; 14 | } 15 | 16 | export function PromptUserContainer({ 17 | title, 18 | description, 19 | action, 20 | icon, 21 | readOnly = false, 22 | containerClassName, 23 | }: PromptUserContainerProps) { 24 | return ( 25 |
33 |
34 | {icon} 35 |
36 |

{title}

37 | {description &&

{description}

} 38 |
39 |
40 | 41 | {action && ( 42 |
43 | 52 |
53 | )} 54 |
55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /sdk/auth0/mgmt.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { DeleteUserIdentityByUserIdProviderEnum, ManagementClient, PostIdentitiesRequestProviderEnum } from "auth0"; 4 | 5 | import { getSession } from "@auth0/nextjs-auth0"; 6 | 7 | const auth0 = new ManagementClient({ 8 | domain: new URL(process.env.AUTH0_ISSUER_BASE_URL!).host, 9 | clientId: process.env.AUTH0_CLIENT_ID_MGMT!, 10 | clientSecret: process.env.AUTH0_CLIENT_SECRET_MGMT!, 11 | }); 12 | 13 | export async function isGuardianEnrolled() { 14 | try { 15 | const session = await getSession(); 16 | const user_id = session?.user.sub; 17 | 18 | const response = await auth0.users.getAuthenticationMethods({ 19 | id: user_id, 20 | }); 21 | const { data } = response; 22 | 23 | return !!data?.find((m) => m.type === "guardian"); 24 | } catch (error) { 25 | console.error(error); 26 | return false; 27 | } 28 | } 29 | 30 | export async function getUser(userId: string) { 31 | return auth0.users.get({ id: userId }); 32 | } 33 | 34 | export async function deleteUser(userId: string) { 35 | return auth0.users.delete({ 36 | id: userId, 37 | }); 38 | } 39 | 40 | export async function linkUser( 41 | userId: string, 42 | identityToAdd: { 43 | provider: PostIdentitiesRequestProviderEnum; 44 | user_id: string; 45 | connection_id?: string; 46 | } 47 | ) { 48 | await auth0.users.link({ id: userId }, identityToAdd); 49 | } 50 | 51 | export async function unlinkUser( 52 | userId: string, 53 | identityToRemove: { 54 | provider: DeleteUserIdentityByUserIdProviderEnum; 55 | user_id: string; 56 | } 57 | ) { 58 | return auth0.users.unlink({ 59 | id: userId, 60 | ...identityToRemove, 61 | }); 62 | } 63 | 64 | export async function updateUser( 65 | userId: string, 66 | { givenName, familyName }: { givenName?: string; familyName?: string } 67 | ) { 68 | return auth0.users.update( 69 | { id: userId }, 70 | { 71 | given_name: givenName ?? undefined, 72 | family_name: familyName ?? undefined, 73 | } 74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Market0 2 | 3 | This is a demo of an interactive financial assistant. It can show you stocks, tell you their prices, and even help you buy shares. 4 | 5 | ## Getting Started 6 | 7 | ### Prerequisites 8 | 9 | - An Auth0 Lab account, you can create one [here](https://manage.auth0lab.com/). 10 | - An Okta FGA account, you can create one [here](https://dashboard.fga.dev). 11 | - An OpenAI account and API key create one [here](https://platform.openai.com). 12 | - Docker to run the postgresql container. 13 | 14 | ### FGA Configuration 15 | 16 | To setup Okta FGA for the sample, create a client with the following permissions: 17 | 18 | - `Read/Write model, changes, and assertions` 19 | - `Write and delete tuples` 20 | - `Read and query` 21 | 22 | ### Auth0 Configuration 23 | 24 | To setup your Auth0 Lab tenant for the sample, create two applications: 25 | 26 | - **Regular Web Application** 27 | 28 | - **Allowed Callback URLs:** `http://localhost:3000/api/auth/callback` 29 | - **Allowed Logout URLs:** `http://localhost:3000` 30 | 31 | - **Machine to Machine** 32 | - **API:** `Auth0 Management API` 33 | - **Permissions:** `read:users update:users delete:users read:authentication_methods` 34 | 35 | Configure [Google](https://marketplace.auth0.com/integrations/google-social-connection) as social connections. 36 | 37 | And enable MFA with [push notifications using Auth0 Guardian](https://auth0.com/docs/secure/multi-factor-authentication/auth0-guardian#enroll-in-push-notifications). 38 | 39 | ### Setup 40 | 41 | 1. Clone this repository to your local machine. 42 | 2. Install the dependencies by running `npm install` in your terminal. 43 | 3. Set up the environment variables making a copy of the [.env-example](./.env-example) file. 44 | 4. Start the database with `npm run db:up` 45 | 5. Configure your FGA store with `npm run fga:migrate:create` 46 | 47 | ### Running the Project 48 | 49 | To start the development server, run `npm run dev` in your terminal. Open [http://localhost:3000](http://localhost:3000) to view the chatbot in your browser. 50 | 51 | ## License 52 | 53 | Apache-2.0 54 | -------------------------------------------------------------------------------- /llm/components/events/events.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { format, parseISO } from "date-fns"; 4 | 5 | import { ExplanationType } from "@/components/explanation/observable"; 6 | import { checkAvailabilityForEvents } from "@/llm/actions/calendar-events"; 7 | 8 | import WarningWrapper from "../warning-wrapper"; 9 | import { CalendarEvents } from "./calendar-events"; 10 | 11 | interface Event { 12 | date: string; 13 | startDate: string; 14 | endDate: string; 15 | headline: string; 16 | description: string; 17 | } 18 | 19 | export function Events({ 20 | events, 21 | companyName, 22 | readOnly = false, 23 | }: { 24 | events: Event[]; 25 | companyName: string; 26 | readOnly?: boolean; 27 | }) { 28 | return ( 29 |
30 | 31 |
32 |
33 | {events.map((event) => ( 34 |
35 |
36 |
37 | {format(parseISO(event.date), "dd LLL, yyyy")} 38 |
39 |
40 | {event.headline.slice(0, 30)} 41 |
42 |
43 |
44 | {event.description.slice(0, 70)}... 45 |
46 |
47 | ))} 48 |
49 |
50 |
51 | 52 | 58 |
59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

41 | )) 42 | CardTitle.displayName = "CardTitle" 43 | 44 | const CardDescription = React.forwardRef< 45 | HTMLParagraphElement, 46 | React.HTMLAttributes 47 | >(({ className, ...props }, ref) => ( 48 |

53 | )) 54 | CardDescription.displayName = "CardDescription" 55 | 56 | const CardContent = React.forwardRef< 57 | HTMLDivElement, 58 | React.HTMLAttributes 59 | >(({ className, ...props }, ref) => ( 60 |

61 | )) 62 | CardContent.displayName = "CardContent" 63 | 64 | const CardFooter = React.forwardRef< 65 | HTMLDivElement, 66 | React.HTMLAttributes 67 | >(({ className, ...props }, ref) => ( 68 |
73 | )) 74 | CardFooter.displayName = "CardFooter" 75 | 76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 77 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata, Viewport } from "next"; 2 | import "./globals.css"; 3 | 4 | import { Inter } from "next/font/google"; 5 | 6 | import { ThemeProvider } from "@/components/theme-provider"; 7 | import { Toaster } from "@/components/ui/toaster"; 8 | import { cn } from "@/lib/utils"; 9 | import { UserProvider } from "@auth0/nextjs-auth0/client"; 10 | 11 | const inter = Inter({ subsets: ["latin"] }); 12 | 13 | export const metadata: Metadata = { 14 | title: "Auth0 AI | Market0", 15 | description: "Market0 is a demo app that showcases secure auth patterns for GenAI apps", 16 | alternates: { 17 | canonical: process.env.AUTH0_BASE_URL, 18 | }, 19 | twitter: { 20 | card: "summary_large_image", 21 | site: "@auth0", 22 | creator: "@auth0", 23 | title: "Auth0 AI | Market0", 24 | description: "Market0 is a demo app that showcases secure auth patterns for GenAI apps", 25 | images: { 26 | url: "https://cdn.auth0.com/website/labs/ai/assets/market0-card.png", 27 | width: 1200, 28 | height: 630, 29 | }, 30 | }, 31 | openGraph: { 32 | type: "website", 33 | siteName: "Auth0 AI | Market0", 34 | title: "Auth0 AI | Market0", 35 | description: "Market0 is a demo app that showcases secure auth patterns for GenAI apps", 36 | locale: "en", 37 | url: process.env.AUTH0_BASE_URL, 38 | images: { 39 | url: "https://cdn.auth0.com/website/labs/ai/assets/market0-card.png", 40 | secureUrl: "https://cdn.auth0.com/website/labs/ai/assets/market0-card.png", 41 | width: 1200, 42 | height: 630, 43 | }, 44 | }, 45 | }; 46 | 47 | export const viewport: Viewport = { 48 | width: "device-width", 49 | initialScale: 1.0, 50 | maximumScale: 1.0, 51 | userScalable: false, 52 | }; 53 | 54 | export default async function RootLayout({ 55 | children, 56 | }: Readonly<{ 57 | children: React.ReactNode; 58 | }>) { 59 | return ( 60 | 61 | 62 | 63 | 64 | {children} 65 | 66 | 67 | 68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /sdk/fga/index.ts: -------------------------------------------------------------------------------- 1 | import { getSession } from "@auth0/nextjs-auth0"; 2 | // https://docs.fga.dev/modeling/basics/custom-roles 3 | import { ClientCheckRequest, CredentialsMethod, OpenFgaClient } from "@openfga/sdk"; 4 | 5 | const host = process.env.FGA_API_HOST || "api.us1.fga.dev"; 6 | 7 | export const fgaClient = new OpenFgaClient({ 8 | apiScheme: "https", 9 | apiHost: host, 10 | storeId: process.env.FGA_STORE_ID, 11 | credentials: { 12 | method: CredentialsMethod.ClientCredentials, 13 | config: { 14 | apiTokenIssuer: process.env.FGA_API_TOKEN_ISSUER || "auth.fga.dev", 15 | apiAudience: `https://${host}/`, 16 | clientId: process.env.FGA_CLIENT_ID!, 17 | clientSecret: process.env.FGA_CLIENT_SECRET!, 18 | }, 19 | }, 20 | }); 21 | 22 | type CheckPermissionParams = { 23 | user: string; 24 | object: string; 25 | relation: string; 26 | context?: object; 27 | }; 28 | 29 | async function checkPermission({ user, object, relation, context }: CheckPermissionParams): Promise { 30 | const check: ClientCheckRequest = { 31 | user, 32 | object, 33 | relation, 34 | }; 35 | 36 | if (context) { 37 | check.context = context; 38 | } 39 | 40 | const { allowed } = await fgaClient.check(check); 41 | 42 | return !!allowed; 43 | } 44 | 45 | export async function getUser() { 46 | const session = await getSession(); 47 | return session?.user!; 48 | } 49 | 50 | /** 51 | * @param params 52 | * @param [params.user] - The user to check permissions for. If not provided, the current user is used. 53 | * @param params.object - The object to check permissions for. 54 | * @param params.relation - The relation to check permissions for. 55 | * @param [params.context] - The context to check permissions for. 56 | */ 57 | export type withFGAParams = { 58 | user?: string; 59 | object: string; 60 | relation: string; 61 | context?: object; 62 | }; 63 | 64 | export async function withFGA(params: withFGAParams) { 65 | const checkPermissionsParams = params as CheckPermissionParams; 66 | 67 | if (params.user === undefined) { 68 | const user = await getUser(); 69 | checkPermissionsParams.user = `user:${user.sub as string}`; 70 | } 71 | 72 | return await checkPermission(checkPermissionsParams); 73 | } 74 | -------------------------------------------------------------------------------- /llm/tools/profile/get-profile.tsx: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | import Loader from "@/components/loader"; 4 | import { checkEnrollment } from "@/llm/actions/newsletter"; 5 | import { defineTool } from "@/llm/ai-helpers"; 6 | import { ProfileCard } from "@/llm/components/profile"; 7 | import * as serialization from "@/llm/components/serialization"; 8 | import { getHistory } from "@/llm/utils"; 9 | import { withTextGeneration } from "@/llm/with-text-generation"; 10 | import { isGuardianEnrolled } from "@/sdk/auth0/mgmt"; 11 | import { fgaClient, getUser } from "@/sdk/fga"; 12 | 13 | import stocks from "../../../lib/market/stocks.json"; 14 | 15 | /** 16 | * This tool allows the user to set their first name and/or last name. 17 | * The changes are propagated to the Auth0's user profile. 18 | */ 19 | export default defineTool("show_me_what_you_know_about_me", async () => { 20 | const history = getHistory(); 21 | 22 | return { 23 | description: `Use this tool to show the user information that the system knows about them.`, 24 | parameters: z.object({}), 25 | generate: async function* ({}) { 26 | yield ; 27 | 28 | const user = await getUser(); 29 | 30 | const { allowed: userWorkForATKO } = await fgaClient.check({ 31 | user: `user:${user.sub}`, 32 | relation: "employee", 33 | object: "company:atko", 34 | }); 35 | 36 | const subscribedToNewsletter = await checkEnrollment({ 37 | symbol: stocks[0].symbol, 38 | }); 39 | 40 | const isEnrolled = await isGuardianEnrolled(); 41 | 42 | const params = { 43 | profile: { 44 | fullName: user.name, 45 | email: user.email, 46 | imageUrl: user.picture, 47 | username: user.nickname, 48 | employment: userWorkForATKO ? stocks[0].longname : "", 49 | subscribedToNewsletter: subscribedToNewsletter ?? false, 50 | enrolledForAsyncAuth: isEnrolled, 51 | }, 52 | readOnly: false, 53 | }; 54 | 55 | history.update({ 56 | role: "assistant", 57 | content: JSON.stringify(params), 58 | componentName: serialization.names.get(ProfileCard)!, 59 | params, 60 | }); 61 | 62 | return ; 63 | }, 64 | }; 65 | }); 66 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | import { withSentryConfig } from "@sentry/nextjs"; 2 | import remarkGfm from "remark-gfm"; 3 | import createMDX from "@next/mdx"; 4 | 5 | const withMDX = createMDX({ 6 | options: { 7 | remarkPlugins: [remarkGfm], 8 | }, 9 | }); 10 | 11 | /** @type {import('next').NextConfig} */ 12 | const nextConfig = { 13 | // Configure `pageExtensions` to include MDX files 14 | pageExtensions: ["js", "jsx", "mdx", "ts", "tsx"], 15 | // Optionally, add any other Next.js config below 16 | images: { 17 | domains: ["cdn.auth0.com"], 18 | }, 19 | experimental: { 20 | instrumentationHook: true, 21 | }, 22 | }; 23 | 24 | export default withSentryConfig(withMDX(nextConfig), { 25 | // For all available options, see: 26 | // https://github.com/getsentry/sentry-webpack-plugin#options 27 | 28 | org: process.env.SENTRY_ORG, 29 | project: process.env.SENTRY_PROJECT, 30 | 31 | // Only print logs for uploading source maps in CI 32 | silent: !process.env.CI, 33 | 34 | // For all available options, see: 35 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ 36 | 37 | // Upload a larger set of source maps for prettier stack traces (increases build time) 38 | widenClientFileUpload: true, 39 | 40 | // Automatically annotate React components to show their full name in breadcrumbs and session replay 41 | reactComponentAnnotation: { 42 | enabled: true, 43 | }, 44 | 45 | // Uncomment to route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers. 46 | // This can increase your server load as well as your hosting bill. 47 | // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client- 48 | // side errors will fail. 49 | // tunnelRoute: "/monitoring", 50 | 51 | // Hides source maps from generated client bundles 52 | hideSourceMaps: true, 53 | 54 | // Automatically tree-shake Sentry logger statements to reduce bundle size 55 | disableLogger: true, 56 | 57 | // Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.) 58 | // See the following for more information: 59 | // https://docs.sentry.io/product/crons/ 60 | // https://vercel.com/docs/cron-jobs 61 | automaticVercelMonitors: true, 62 | }); 63 | -------------------------------------------------------------------------------- /app/docs/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { Header } from "@/components/chat/header"; 2 | 3 | import { Content } from "./content"; 4 | 5 | import type { Metadata, ResolvingMetadata } from "next"; 6 | 7 | type Props = { 8 | params: Promise<{ id: string }>; 9 | searchParams: Promise<{ [key: string]: string | string[] | undefined }>; 10 | }; 11 | 12 | export async function generateMetadata({ params }: Props, parent: ResolvingMetadata): Promise { 13 | // read route params 14 | const id = (await params).id; 15 | 16 | const cardMapper: Record = { 17 | "call-apis-on-users-behalf": "https://cdn.auth0.com/website/labs/ai/assets/use-case-2-card.png", 18 | "authorization-for-rag": "https://cdn.auth0.com/website/labs/ai/assets/use-case-3-card.png", 19 | "async-user-confirmation": "https://cdn.auth0.com/website/labs/ai/assets/use-case-4-card.png", 20 | }; 21 | 22 | const image = cardMapper[id] || cardMapper["call-apis-on-users-behalf"]; 23 | 24 | return { 25 | title: "Auth0 AI | Market0", 26 | description: "Market0 is a demo app that showcases secure auth patterns for GenAI apps", 27 | alternates: { 28 | canonical: `${process.env.AUTH0_BASE_URL}/docs/${id}`, 29 | }, 30 | twitter: { 31 | card: "summary_large_image", 32 | site: "@auth0", 33 | creator: "@auth0", 34 | title: "Auth0 AI | Market0", 35 | description: "Market0 is a demo app that showcases secure auth patterns for GenAI apps", 36 | images: { 37 | url: image, 38 | width: 1200, 39 | height: 630, 40 | }, 41 | }, 42 | openGraph: { 43 | type: "website", 44 | siteName: "Auth0 AI | Market0", 45 | title: "Auth0 AI | Market0", 46 | description: "Market0 is a demo app that showcases secure auth patterns for GenAI apps", 47 | locale: "en", 48 | url: `${process.env.AUTH0_BASE_URL}/docs/${id}`, 49 | images: { 50 | url: image, 51 | secureUrl: image, 52 | width: 1200, 53 | height: 630, 54 | }, 55 | }, 56 | }; 57 | } 58 | 59 | export default function Page({ params: { id } }: { params: { id: string } }) { 60 | return ( 61 |
62 |
63 | 64 |
65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /hooks/auth0/use-connected-accounts.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react"; 2 | 3 | export default function useConnectedAccounts() { 4 | const fetchConnectedAccounts = useCallback(async (): Promise<{ 5 | connectedAccounts?: { 6 | provider: string; 7 | connection: string; 8 | isPrimary: boolean; 9 | }[]; 10 | status: number; 11 | }> => { 12 | try { 13 | const response = await fetch("/api/auth/user/accounts", { 14 | method: "GET", 15 | headers: { 16 | "Content-Type": "application/json", 17 | }, 18 | }); 19 | 20 | // TODO: Better handling rate limits 21 | if (response.status === 429) { 22 | return { status: 429 }; 23 | } 24 | 25 | return { 26 | connectedAccounts: await response.json(), 27 | status: response.status, 28 | }; 29 | } catch (error) { 30 | console.error(error); 31 | return { status: 500 }; 32 | } 33 | }, []); 34 | 35 | const deleteUserAccount = useCallback( 36 | async ( 37 | connection: string 38 | ): Promise<{ 39 | status: number; 40 | }> => { 41 | try { 42 | const response1 = await fetch("/api/auth/user/accounts", { 43 | method: "GET", 44 | headers: { 45 | "Content-Type": "application/json", 46 | }, 47 | }); 48 | 49 | // TODO: Better handling rate limits 50 | if (response1.status === 429) { 51 | return { status: 429 }; 52 | } 53 | 54 | const accounts = await response1.json(); 55 | const { provider, userId } = accounts.find( 56 | (acc: { connection: string }) => acc.connection === connection 57 | ); 58 | 59 | const response2 = await fetch( 60 | `/api/auth/user/accounts/${provider}/${userId}`, 61 | { 62 | method: "DELETE", 63 | } 64 | ); 65 | 66 | // TODO: Better handling rate limits 67 | if (response2.status === 429) { 68 | return { status: 429 }; 69 | } 70 | 71 | return { status: response2.status }; 72 | } catch (error) { 73 | console.error(error); 74 | return { status: 500 }; 75 | } 76 | }, 77 | [] 78 | ); 79 | 80 | return { fetchConnectedAccounts, deleteUserAccount }; 81 | } 82 | -------------------------------------------------------------------------------- /mdx-components.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @next/next/no-img-element */ 2 | import type { MDXComponents } from "mdx/types"; 3 | import React from "react"; 4 | 5 | import { CodeBlock, UseCase } from "./components/explanation/explanation-mdx"; 6 | import { Auth0Icon } from "./components/icons"; 7 | 8 | // This file allows you to provide custom React components 9 | // to be used in MDX files. You can import and use any 10 | // React component you want, including inline styles, 11 | // components from other libraries, and more. 12 | 13 | export function useMDXComponents(components: MDXComponents): MDXComponents { 14 | return { 15 | // Allows customizing built-in components, e.g. to add styling. 16 | h2: ({ children }) =>

{children}

, 17 | h3: ({ children }) => ( 18 |

{children}

19 | ), 20 | h4: ({ children }) =>

{children}

, 21 | a: ({ children, href }) => ( 22 | 23 | {children} 24 | 25 | ), 26 | ol: ({ children }) => ( 27 |
    28 | {children} 29 |
30 | ), 31 | ul: ({ children }) => ( 32 |
    33 | {children} 34 |
35 | ), 36 | li: ({ children }) =>
  • {children}
  • , 37 | strong: ({ children }) => {children}, 38 | p: ({ children }) =>

    {children}

    , 39 | code: ({ children }) => {children}, 40 | table: ({ children }) => {children}
    , 41 | th: ({ children }) => {children}, 42 | td: ({ children }) => {children}, 43 | 44 | Auth0Icon: () => , 45 | UseCase, 46 | CodeBlock, 47 | 48 | ...components, 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /app/api/hooks/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | 3 | import { transactions } from "@/lib/db"; 4 | import { getByID, getMatchingPurchases, update } from "@/lib/db/conditional-purchases"; 5 | import { getStockPrices } from "@/lib/market/stocks"; 6 | import { pollMode } from "@/sdk/auth0/ciba"; 7 | 8 | function getRandomPrice(min: number, max: number): number { 9 | return Math.random() * (max - min) + min; 10 | } 11 | 12 | export async function POST(request: NextRequest) { 13 | const { type, data } = await request.json(); 14 | 15 | // TODO: implement a proper payload validation 16 | if (type !== "metric") { 17 | return NextResponse.json(`Unsupported type: ${type} (expected: 'metric').`, { status: 400 }); 18 | } 19 | 20 | if (!data) { 21 | return NextResponse.json( 22 | `A 'data' object with 'symbol', 'metric', 'value' and (optionally) 'user_id' properties, or with 'conditional_purchase_id' and 'user_id' properties is required.`, 23 | { status: 400 } 24 | ); 25 | } 26 | 27 | // TODO: in a real-world scenario, this task should be send to a queue 28 | const matchingPurchases = data.conditional_purchase_id 29 | ? [await getByID(data.user_id, data.conditional_purchase_id)].filter((p) => p !== null) 30 | : await getMatchingPurchases(data.symbol, data.metric, data.value, data.user_id); 31 | 32 | console.log("matchingPurchases", matchingPurchases); 33 | 34 | await Promise.all( 35 | matchingPurchases.map(async (purchase) => { 36 | try { 37 | const price = 38 | data.metric === "price" ? data.value : (await getStockPrices({ symbol: purchase.symbol })).current_price; 39 | 40 | // wait for user's approval 41 | const { accessToken } = await pollMode(purchase.user_id); 42 | // TODO: in a real-world scenario, you will take the user access token to perform the purchase 43 | 44 | await transactions.create(purchase.symbol, price, purchase.quantity, "buy", purchase.user_id); 45 | await update(purchase.id, { status: "completed" }); 46 | console.log(`completed purchase with id ${purchase.id}`); 47 | } catch (err) { 48 | await update(purchase.id, { status: "canceled" }); 49 | console.log(`canceled purchase with id ${purchase.id}`, err); 50 | } 51 | }) 52 | ); 53 | 54 | return new NextResponse(); 55 | } 56 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | --card: 0 0% 100%; 10 | --card-foreground: 222.2 84% 4.9%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 222.2 84% 4.9%; 13 | --primary: 222.2 47.4% 11.2%; 14 | --primary-foreground: 210 40% 98%; 15 | --secondary: 210 40% 96.1%; 16 | --secondary-foreground: 222.2 47.4% 11.2%; 17 | --muted: 210 40% 96.1%; 18 | --muted-foreground: 215.4 16.3% 46.9%; 19 | --accent: 210 40% 96.1%; 20 | --accent-foreground: 222.2 47.4% 11.2%; 21 | --destructive: 0 84.2% 60.2%; 22 | --destructive-foreground: 210 40% 98%; 23 | --border: 214.3 31.8% 91.4%; 24 | --input: 214.3 31.8% 91.4%; 25 | --ring: 222.2 84% 4.9%; 26 | --radius: 0.5rem; 27 | --chart-1: 12 76% 61%; 28 | --chart-2: 173 58% 39%; 29 | --chart-3: 197 37% 24%; 30 | --chart-4: 43 74% 66%; 31 | --chart-5: 27 87% 67%; 32 | } 33 | 34 | .dark { 35 | --background: 222.2 84% 4.9%; 36 | --foreground: 210 40% 98%; 37 | --card: 222.2 84% 4.9%; 38 | --card-foreground: 210 40% 98%; 39 | --popover: 222.2 84% 4.9%; 40 | --popover-foreground: 210 40% 98%; 41 | --primary: 210 40% 98%; 42 | --primary-foreground: 222.2 47.4% 11.2%; 43 | --secondary: 217.2 32.6% 17.5%; 44 | --secondary-foreground: 210 40% 98%; 45 | --muted: 217.2 32.6% 17.5%; 46 | --muted-foreground: 215 20.2% 65.1%; 47 | --accent: 217.2 32.6% 17.5%; 48 | --accent-foreground: 210 40% 98%; 49 | --destructive: 0 62.8% 30.6%; 50 | --destructive-foreground: 210 40% 98%; 51 | --border: 217.2 32.6% 17.5%; 52 | --input: 217.2 32.6% 17.5%; 53 | --ring: 212.7 26.8% 83.9%; 54 | --chart-1: 220 70% 50%; 55 | --chart-2: 160 60% 45%; 56 | --chart-3: 30 80% 55%; 57 | --chart-4: 280 65% 60%; 58 | --chart-5: 340 75% 55%; 59 | } 60 | } 61 | 62 | @layer base { 63 | * { 64 | @apply border-border; 65 | } 66 | body { 67 | @apply bg-background text-foreground; 68 | } 69 | } 70 | 71 | .markdown, 72 | .markdown * { 73 | /* remove all tailwind */ 74 | all: revert !important; 75 | } 76 | 77 | .markdown p:first-child { 78 | margin-top: 0px !important; 79 | } 80 | 81 | .markdown p:last-child { 82 | margin-bottom: 0px !important; 83 | } 84 | -------------------------------------------------------------------------------- /sdk/auth0/3rd-party-apis/providers/google.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { getSession } from "@auth0/nextjs-auth0"; 4 | 5 | const fetchAccessToken = async (auth0IdToken: string): Promise => { 6 | const connectionName = process.env.NEXT_PUBLIC_GOOGLE_CONNECTION_NAME; 7 | const requestedTokenTypeBase = "http://auth0.com/oauth/token-type/social-access-token/google-oauth2"; 8 | const requested_token_type = connectionName ? `${requestedTokenTypeBase}/${connectionName}` : requestedTokenTypeBase; 9 | 10 | const res = await fetch(`${process.env.AUTH0_ISSUER_BASE_URL}/oauth/token`, { 11 | method: "POST", 12 | headers: { "Content-Type": "application/json" }, 13 | body: JSON.stringify({ 14 | grant_type: "urn:ietf:params:oauth:grant-type:token-exchange", 15 | client_id: process.env.AUTH0_CLIENT_ID, 16 | client_secret: process.env.AUTH0_CLIENT_SECRET, 17 | subject_token_type: "urn:ietf:params:oauth:token-type:id_token", 18 | subject_token: auth0IdToken, 19 | requested_token_type, 20 | }), 21 | }); 22 | 23 | if (!res.ok) { 24 | throw new Error(`Unable to get a Google API access token: ${await res.text()}`); 25 | } 26 | 27 | const { access_token } = await res.json(); 28 | return access_token; 29 | }; 30 | 31 | export async function verifyAccessToken(accessToken: string, scopesToCheck: string[] | string): Promise { 32 | const res = await fetch(`https://oauth2.googleapis.com/tokeninfo?access_token=${accessToken}`); 33 | 34 | if (!res.ok) { 35 | console.log(`Unable to verify Google API access token: ${await res.text()}`); 36 | return false; 37 | } 38 | 39 | const tokenInfo = await res.json(); 40 | const tokenScopes = tokenInfo.scope.split(" "); 41 | 42 | return (Array.isArray(scopesToCheck) ? scopesToCheck : [scopesToCheck]).every((scope) => tokenScopes.includes(scope)); 43 | } 44 | 45 | export async function getAccessToken() { 46 | "use server"; 47 | const session = await getSession(); 48 | const auth0IdToken = session?.idToken!; 49 | let accessToken = undefined; 50 | 51 | try { 52 | accessToken = await fetchAccessToken(auth0IdToken); 53 | return accessToken; 54 | } catch (e) { 55 | console.error(e); 56 | } 57 | 58 | return accessToken; 59 | } 60 | 61 | export async function withGoogleApi(fn: (accessToken: string) => Promise) { 62 | const accessToken = await getAccessToken(); 63 | return await fn(accessToken!); 64 | } 65 | -------------------------------------------------------------------------------- /components/welcome/Welcome.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import React from "react"; 3 | 4 | import { buttonVariants } from "@/components/ui/button"; 5 | import { Card, CardContent } from "@/components/ui/card"; 6 | 7 | import { IconAuth0, WelcomeIcon } from "../icons"; 8 | 9 | const MainContent = () => ( 10 |
    11 |
    12 |

    13 | Market0 is a demo application by{" "} 14 | 15 | auth0.ai 16 | 17 | , designed to showcase how Gen AI can drive advanced authentication and authorization flows. 18 |

    19 | 20 |

    21 | Please log in to access the demo and explore real-world use cases. 22 |

    23 |
    24 | 25 | 31 | Log In to Start Demo 32 | 33 |
    34 | ); 35 | 36 | const WelcomeScreen = () => { 37 | return ( 38 |
    39 |
    40 | 41 | 42 | 43 |
    44 | 45 |
    46 |
    47 |
    48 | 49 |
    50 | 51 |

    52 | Explore the Future of Authentication with Market0 Demo App 53 |

    54 |
    55 |
    56 | 57 | 58 | 59 | 60 | 61 |
    62 | ); 63 | }; 64 | 65 | export default WelcomeScreen; 66 | -------------------------------------------------------------------------------- /llm/tools/events/get-events.tsx: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | import { getCompanyInfo } from "@/lib/market/stocks"; 4 | import { defineTool } from "@/llm/ai-helpers"; 5 | import { Events, EventsSkeleton } from "@/llm/components/events"; 6 | import * as serialization from "@/llm/components/serialization"; 7 | import { getHistory } from "@/llm/utils"; 8 | 9 | /** 10 | * This tool is used to get upcoming events for a stock. 11 | * The events are automatically generated by the LLM. 12 | */ 13 | export default defineTool("get_events", () => { 14 | const history = getHistory(); 15 | 16 | return { 17 | description: `List up to 3 fictional stock events occurring after the start date '${new Date().toLocaleDateString( 18 | "en-CA" 19 | )}' and between user-specified dates.`, 20 | parameters: z.object({ 21 | symbol: z.string().describe("The name or symbol of the stock. e.g. DOGE/AAPL/USD."), 22 | events: z.array( 23 | z.object({ 24 | date: z.string().describe("The date of the event, only weekdays, in ISO-8601 format"), 25 | startDate: z 26 | .string() 27 | .describe( 28 | "The start time of the event in ISO 8601 format (UTC), ensuring it falls on a weekday and during standard working hours (9 AM to 5 PM)" 29 | ), 30 | endDate: z 31 | .string() 32 | .describe( 33 | "The end time of the event in ISO 8601 format (UTC). The end time should be no more than 2 hours after the start time and should have a random duration of at least 45 minutes." 34 | ), 35 | headline: z.string().describe("The headline of the event"), 36 | description: z.string().describe("The description of the event"), 37 | }) 38 | ), 39 | }), 40 | generate: async function* ({ events, symbol }) { 41 | yield ; 42 | 43 | const doc = await getCompanyInfo({ symbol }); 44 | 45 | if (!doc) { 46 | history.update(`[Company not found for ${symbol}]`); 47 | return <>Company not found; 48 | } 49 | 50 | history.update({ 51 | role: "assistant", 52 | componentName: serialization.names.get(Events)!, 53 | // intentionally avoid 54 | params: { events, companyName: doc.shortname }, 55 | content: `[Listed events: ${JSON.stringify({ events })}]`, 56 | }); 57 | 58 | return ; 59 | }, 60 | }; 61 | }); 62 | -------------------------------------------------------------------------------- /llm/components/stocks/stocks.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useActions, useUIState } from "ai/rsc"; 4 | 5 | import { StockDownIcon, StockUpIcon } from "@/components/icons"; 6 | import { cn } from "@/lib/utils"; 7 | 8 | import { ClientMessage } from "../../types"; 9 | import WarningWrapper from "../warning-wrapper"; 10 | 11 | export function Stocks({ stocks, readOnly = false }: { stocks: any[]; readOnly?: boolean }) { 12 | const [, setMessages] = useUIState(); 13 | const { continueConversation } = useActions(); 14 | 15 | return ( 16 | 17 |
    18 | {stocks.map((stock) => ( 19 | 54 | ))} 55 |
    56 |
    57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /sdk/components/ensure-api-access.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ReactNode, useCallback, useEffect, useState } from "react"; 4 | 5 | import { getThirdPartyContext, Provider } from "../auth0/3rd-party-apis"; 6 | import { ConnectGoogleAccount } from "./connect-google-account"; 7 | import Loader from "./loader"; 8 | 9 | type ShouldCheckAuthorizationHandler = () => boolean | Promise; 10 | 11 | type EnsureAPIAccessProps = { 12 | children: ReactNode; 13 | provider: Provider; 14 | connectWidget: { 15 | icon?: ReactNode; 16 | title: string; 17 | description: string; 18 | action?: { label: string }; 19 | }; 20 | onUserAuthorized?: () => Promise; 21 | shouldCheckAuthorization: boolean | ShouldCheckAuthorizationHandler; 22 | readOnly?: boolean; 23 | }; 24 | 25 | export function EnsureAPIAccess({ 26 | children, 27 | provider, 28 | connectWidget: { title, description, icon, action }, 29 | onUserAuthorized, 30 | shouldCheckAuthorization, 31 | readOnly, 32 | }: EnsureAPIAccessProps) { 33 | const [isWorking, setIsWorking] = useState(true); 34 | const [isLoginRequired, setLoginRequired] = useState(false); 35 | 36 | const init = useCallback(async () => { 37 | const shouldCheck = 38 | typeof shouldCheckAuthorization === "function" ? await shouldCheckAuthorization() : shouldCheckAuthorization; 39 | 40 | if (shouldCheck) { 41 | const ctx = await getThirdPartyContext({ 42 | providers: [ 43 | { 44 | name: provider.name, 45 | api: provider.api, 46 | requiredScopes: provider.requiredScopes, 47 | }, 48 | ], 49 | }); 50 | 51 | if (!ctx.google.containsRequiredScopes) { 52 | setLoginRequired(true); 53 | } else { 54 | setLoginRequired(false); 55 | 56 | if (typeof onUserAuthorized === "function") { 57 | await onUserAuthorized(); 58 | } 59 | } 60 | } 61 | 62 | setIsWorking(false); 63 | // eslint-disable-next-line react-hooks/exhaustive-deps 64 | }, []); 65 | 66 | useEffect(() => { 67 | init(); 68 | }, [init]); 69 | 70 | if (isWorking) { 71 | return ; 72 | } 73 | 74 | if (isLoginRequired) { 75 | return ( 76 | 84 | ); 85 | } 86 | 87 | return <>{children}; 88 | } 89 | -------------------------------------------------------------------------------- /routers/user-accounts.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { getSession, withApiAuthRequired } from "@auth0/nextjs-auth0"; 3 | 4 | import { withRateLimit } from "@/hooks/auth0/helpers/rate-limit"; 5 | import { deleteUser, getUser, unlinkUser } from "@/sdk/auth0/mgmt"; 6 | import { DeleteUserIdentityByUserIdProviderEnum } from "auth0"; 7 | 8 | export function handleUserAccountsFetch() { 9 | return withRateLimit( 10 | withApiAuthRequired(async (): Promise => { 11 | try { 12 | const session = await getSession(); 13 | const mainUserId = session?.user.sub; 14 | const response = await getUser(mainUserId); 15 | const { data } = response; 16 | 17 | return NextResponse.json( 18 | data.identities.map( 19 | ({ connection, provider, user_id: userId }, idx) => ({ 20 | connection, 21 | provider, 22 | userId, 23 | isPrimary: idx === 0, 24 | }) 25 | ), 26 | { 27 | status: response.status, 28 | } 29 | ); 30 | } catch (error) { 31 | console.error(error); 32 | return NextResponse.json( 33 | { error: "Error fetching user accounts" }, 34 | { status: 500 } 35 | ); 36 | } 37 | }) 38 | ); 39 | } 40 | 41 | export function handleDeleteUserAccount() { 42 | return withRateLimit( 43 | withApiAuthRequired( 44 | async (request: Request, { params }: any): Promise => { 45 | try { 46 | const { 47 | provider, 48 | user_id, 49 | }: { 50 | provider: DeleteUserIdentityByUserIdProviderEnum; 51 | user_id: string; 52 | } = params; 53 | const session = await getSession(); 54 | const mainUserId = session?.user.sub; 55 | 56 | const response1 = await unlinkUser(mainUserId, { 57 | provider, 58 | user_id, 59 | }); 60 | 61 | if (response1.status !== 200) { 62 | return NextResponse.json({ 63 | status: response1.status, 64 | }); 65 | } 66 | 67 | const response2 = await deleteUser(`${provider}|${user_id}`); 68 | 69 | return NextResponse.json({ 70 | status: response2.status, 71 | }); 72 | } catch (error) { 73 | console.error(error); 74 | return NextResponse.json( 75 | { error: "Error deleting user account" }, 76 | { status: 500 } 77 | ); 78 | } 79 | } 80 | ) 81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /llm/actions/newsletter.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { documents } from "@/lib/db"; 4 | import { fgaClient, getUser } from "@/sdk/fga"; 5 | import { Claims } from "@auth0/nextjs-auth0"; 6 | import { ConsistencyPreference } from "@openfga/sdk"; 7 | 8 | type Document = documents.Document; 9 | 10 | /** 11 | * Generates an array of tuples representing the relationship between a user and a list of documents. 12 | * 13 | * @param user - The user claims object containing user information. 14 | * @param docs - An array of documents to generate tuples for. 15 | * @returns An array of objects, each representing a tuple with the user, document, and relation. 16 | */ 17 | function generateTuples(user: Claims, docs: Document[]) { 18 | return docs.map((doc) => { 19 | return { 20 | user: `user:${user.sub}`, 21 | object: `doc:${doc.metadata.id}`, 22 | relation: "can_view", 23 | }; 24 | }); 25 | } 26 | 27 | async function batchCheckTuples(tuples: any[]) { 28 | const { responses: batchCheckResult } = await fgaClient.batchCheck(tuples); 29 | return batchCheckResult; 30 | } 31 | 32 | async function getDocs(symbol?: string) { 33 | return symbol ? await documents.query("forecast", symbol) : await documents.query("forecast"); 34 | } 35 | 36 | export async function enrollToNewsletter() { 37 | const user = await getUser(); 38 | const docs = await getDocs(); 39 | const tuples = generateTuples(user, docs); 40 | const batchCheckResult = await batchCheckTuples(tuples); 41 | const createTuples = tuples.filter((_tuple, index) => !batchCheckResult[index].allowed); 42 | 43 | await fgaClient.write({ 44 | writes: createTuples, 45 | }); 46 | } 47 | 48 | export async function unenrollFromNewsletter() { 49 | const user = await getUser(); 50 | const docs = await getDocs(); 51 | const tuples = generateTuples(user, docs); 52 | const batchCheckResult = await batchCheckTuples(tuples); 53 | const deleteTuples = tuples.filter((_tuple, index) => batchCheckResult[index].allowed); 54 | 55 | await fgaClient.deleteTuples(deleteTuples); 56 | } 57 | 58 | export async function checkEnrollment({ symbol }: { symbol: string }) { 59 | const user = await getUser(); 60 | const docs = await documents.query("forecast", symbol); 61 | if (docs.length === 0) { 62 | return false; 63 | } 64 | 65 | //Test if the user is allowed to view any forecast. 66 | const { allowed } = await fgaClient.check( 67 | { 68 | user: `user:${user.sub}`, 69 | object: `doc:${docs[0].metadata.id}`, 70 | relation: "can_view", 71 | }, 72 | { 73 | consistency: ConsistencyPreference.HigherConsistency, 74 | } 75 | ); 76 | 77 | return allowed; 78 | } 79 | -------------------------------------------------------------------------------- /app/read/[id]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { notFound, redirect } from "next/navigation"; 2 | 3 | import { AI, fetchUserById } from "@/app/actions"; 4 | import { isChatOwner, isCurrentUserInvitedToChat, setCurrentUserAsReader } from "@/app/chat/[id]/actions"; 5 | import { ChatProvider } from "@/components/chat/context"; 6 | import ConversationPicker from "@/components/chat/conversation-picker"; 7 | import { Header } from "@/components/chat/header"; 8 | import { ErrorContainer } from "@/components/fga/error"; 9 | import { conversations } from "@/lib/db"; 10 | import { getUser, withFGA } from "@/sdk/fga"; 11 | import { withCheckPermission } from "@/sdk/fga/next/with-check-permission"; 12 | import { UserProvider } from "@auth0/nextjs-auth0/client"; 13 | 14 | type RootChatParams = Readonly<{ 15 | children: React.ReactNode; 16 | params: { 17 | id: string; 18 | }; 19 | }>; 20 | 21 | async function RootLayout({ children, params }: RootChatParams) { 22 | const [conversation, isOwner, user] = await Promise.all([ 23 | conversations.get(params.id), 24 | isChatOwner(params.id), 25 | getUser(), 26 | ]); 27 | 28 | if (!conversation) { 29 | return notFound(); 30 | } 31 | 32 | const { messages, ownerID } = conversation; 33 | const chatOwnerID = ownerID || (isOwner ? user?.sub : undefined); 34 | const ownerProfile = chatOwnerID ? await fetchUserById(chatOwnerID) : undefined; 35 | 36 | return ( 37 | 38 | 0} ownerProfile={ownerProfile}> 39 | 40 |
    41 |
    }>
    42 | {children} 43 |
    44 |
    45 |
    46 |
    47 | ); 48 | } 49 | 50 | export default withCheckPermission( 51 | { 52 | checker: async ({ params }: RootChatParams) => { 53 | const chatId = params.id; 54 | const allowed = await withFGA({ 55 | object: `chat:${chatId}`, 56 | relation: "can_view", 57 | }); 58 | 59 | if (!allowed && (await isCurrentUserInvitedToChat(chatId))) { 60 | await setCurrentUserAsReader(chatId); 61 | return true; 62 | } 63 | 64 | return allowed; 65 | }, 66 | onUnauthorized: ({ params }: RootChatParams) => ( 67 | 68 |
    69 | 70 | 71 | ), 72 | }, 73 | RootLayout 74 | ); 75 | -------------------------------------------------------------------------------- /app/api/auth/[auth0]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next"; 2 | 3 | import { handle3rdPartyParams } from "@/sdk/auth0/3rd-party-apis"; 4 | import { linkUser } from "@/sdk/auth0/mgmt"; 5 | import { getSession, handleAuth, handleCallback, handleLogin, LoginOptions } from "@auth0/nextjs-auth0"; 6 | 7 | export const GET = handleAuth({ 8 | login: async (req: NextApiRequest, res: NextApiResponse) => { 9 | const session = await getSession(); 10 | const user = session?.user; 11 | 12 | const url = new URL(req.url!); 13 | const thirdPartyApi = url.searchParams.get("3rdPartyApi") || ""; 14 | const linkWith = url.searchParams.get("linkWith"); 15 | const screenHint = url.searchParams.get("screenHint"); 16 | 17 | const authorizationParams = { 18 | ...(screenHint && { screen_hint: screenHint }), 19 | ...(thirdPartyApi && (await handle3rdPartyParams(thirdPartyApi))), 20 | ...(linkWith && { 21 | connection: linkWith, 22 | login_hint: user?.email, 23 | }), 24 | }; 25 | 26 | return await handleLogin(req, res, { 27 | authorizationParams, 28 | async getLoginState(_req: any, options: LoginOptions) { 29 | if (linkWith) { 30 | // store user id to be used during linking account from next login callback 31 | return user && { primaryUserId: user.sub, secondaryProvider: linkWith }; 32 | } 33 | 34 | return {}; 35 | }, 36 | }); 37 | }, 38 | callback: handleCallback({ 39 | afterCallback: async (_req: any, session: any, state: { [key: string]: any }) => { 40 | const { primaryUserId, secondaryProvider } = state; 41 | const { user } = session; 42 | 43 | if (primaryUserId && primaryUserId !== user.sub) { 44 | console.log(`linking ${primaryUserId} with ${user.sub}`); 45 | 46 | // TODO: this approach is allowing multiple identities for the same connection, should we restrict it to 1 identity per connection? 47 | let identityToAdd = { 48 | provider: secondaryProvider, 49 | user_id: user.sub, 50 | }; 51 | 52 | // adding this because we have multiple google connections 53 | if ( 54 | identityToAdd.provider === process.env.NEXT_PUBLIC_GOOGLE_CONNECTION_NAME && 55 | process.env.GOOGLE_CONNECTION_ID 56 | ) { 57 | identityToAdd.provider = "google-oauth2"; 58 | // @ts-ignore 59 | identityToAdd.connection_id = process.env.GOOGLE_CONNECTION_ID; 60 | } 61 | 62 | await linkUser(primaryUserId, identityToAdd); 63 | 64 | // force a silent login to get a fresh session for the updated user profile 65 | state.returnTo = `/api/auth/login?returnTo=${encodeURI(state.returnTo)}`; 66 | } 67 | 68 | return session; 69 | }, 70 | }), 71 | }); 72 | -------------------------------------------------------------------------------- /llm/actions/calendar-events.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { withGoogleApi } from "@/sdk/auth0/3rd-party-apis/providers/google"; 4 | 5 | export type Event = { 6 | headline: string; 7 | description: string; 8 | date: string; 9 | startDate: string; 10 | endDate: string; 11 | }; 12 | 13 | export type EventAvailability = Event & { slotAvailable: boolean }; 14 | 15 | async function checkAvailability(accessToken: string, event: Event) { 16 | const url = "https://www.googleapis.com/calendar/v3/freeBusy"; 17 | const response = await fetch(url, { 18 | method: "POST", 19 | headers: { 20 | Authorization: `Bearer ${accessToken}`, 21 | "Content-Type": "application/json", 22 | }, 23 | body: JSON.stringify({ 24 | timeMin: event.startDate, 25 | timeMax: event.endDate, 26 | timeZone: "UTC", 27 | items: [{ id: "primary" }], 28 | }), 29 | }); 30 | 31 | if (!response.ok) { 32 | return { ...event, slotAvailable: false }; 33 | } 34 | 35 | const data = await response.json(); 36 | const busy = data.calendars.primary.busy; 37 | const eventMeta = { ...event, slotAvailable: busy.length === 0 }; 38 | 39 | return eventMeta; 40 | } 41 | 42 | export async function checkAvailabilityForEvents(events: Event[]) { 43 | return withGoogleApi(async function (accessToken: string) { 44 | return await Promise.all( 45 | events.map(async (event: Event) => { 46 | return checkAvailability(accessToken, event); 47 | }) 48 | ); 49 | }); 50 | } 51 | 52 | export async function addEventsToCalendar(events: Event[]) { 53 | return withGoogleApi(async function (accessToken: string) { 54 | return await Promise.all( 55 | events.map(async (event: Event) => { 56 | // https://developers.google.com/calendar/api/v3/reference/events/insert 57 | const res = await fetch("https://www.googleapis.com/calendar/v3/calendars/primary/events", { 58 | method: "POST", 59 | headers: { 60 | Authorization: `Bearer ${accessToken}`, 61 | "Content-Type": "application/json", 62 | }, 63 | body: JSON.stringify({ 64 | summary: event.headline, 65 | description: event.description, 66 | start: { 67 | date: event.date, 68 | timeZone: "UTC", 69 | }, 70 | end: { 71 | date: event.date, 72 | timeZone: "UTC", 73 | }, 74 | reminders: { 75 | useDefault: true, 76 | }, 77 | }), 78 | }); 79 | 80 | if (!res.ok) { 81 | throw new Error(`Error creating event: ${await res.text()}`); 82 | } 83 | 84 | const { summary: headline, htmlLink } = await res.json(); 85 | return { headline, htmlLink }; 86 | }) 87 | ); 88 | }); 89 | } 90 | -------------------------------------------------------------------------------- /components/with-toolbar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useRef, useState } from "react"; 4 | 5 | import { ExplanationMeta, ExplanationType, useObservable } from "@/components/explanation/observable"; 6 | import { Button } from "@/components/ui/button"; 7 | 8 | export function WithToolbar({ children }: { children: React.ReactNode }) { 9 | const { setExplanation } = useObservable(); 10 | const [currentExplanation, setCurrentExplanation] = useState(null); 11 | const ref = useRef(null); 12 | 13 | function handleClick() { 14 | if (currentExplanation) { 15 | currentExplanation.expand = true; 16 | setExplanation(currentExplanation); 17 | } 18 | } 19 | 20 | function updateExplanation($element: HTMLDivElement) { 21 | const data = $element.querySelector("[data-explanation]")?.getAttribute("data-explanation"); 22 | setCurrentExplanation({ type: data as ExplanationType }); 23 | } 24 | 25 | useEffect(() => { 26 | const $element = ref.current; 27 | 28 | if ($element) { 29 | updateExplanation($element); 30 | 31 | const observer = new MutationObserver(() => { 32 | updateExplanation($element); 33 | }); 34 | 35 | observer.observe($element, { childList: true, subtree: true }); 36 | } 37 | }, []); 38 | 39 | return ( 40 |
    41 |
    {children}
    42 | 43 | {currentExplanation?.type && ( 44 |
    45 | 71 |
    72 | )} 73 |
    74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /scripts/llm/insert-forecasts.ts: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | import type { Document } from "@langchain/core/documents"; 3 | import stocks from "@/lib/market/stocks.json"; 4 | 5 | 6 | dotenv.config({ path: ".env.local" }); 7 | 8 | import { getDocumentsVectorStore } from "../../lib/documents"; 9 | import { documents } from "../../lib/db"; 10 | import { openai } from "@ai-sdk/openai"; 11 | import { generateId, generateText } from "ai"; 12 | 13 | /** 14 | * The purpose of this file is to generate forecast reports for the next quarter 15 | * for each stock in the stocks.json file. 16 | * 17 | * We use the OpenAI API to generate the forecast reports. 18 | */ 19 | 20 | const generateForecast = async ({ 21 | symbol, 22 | name, 23 | summary, 24 | lastEarning 25 | }: { 26 | symbol: string, 27 | name: string, 28 | summary: string, 29 | lastEarning: string 30 | }) => { 31 | const sentiment: 'bullish' | 'bearish' = Math.random() > 0.5 ? 'bullish' : 'bearish'; 32 | const marketTrend = sentiment === 'bullish' ? 'outperform' : 'underperform'; 33 | const { text } = await generateText({ 34 | model: openai("gpt-4o"), 35 | temperature: 0.5, 36 | system: `You are a ficticious financial analyst. 37 | 38 | You are writing a report on a ficticious company called ${name} (ticker: ${symbol}) for the next fiscal year. 39 | 40 | The ficticious company summary is: 41 | ${summary} 42 | 43 | You are ${sentiment} on the stock and believe it will ${marketTrend} the market. 44 | 45 | You are writing a report on the company's financial performance and future prospects. 46 | 47 | The last earning report was: 48 | ${lastEarning} 49 | `, 50 | prompt: `Generate a solid and convincing forecast for ${symbol} for the next fiscal year.`, 51 | }); 52 | return text; 53 | } 54 | 55 | async function main() { 56 | const vectorStore = await getDocumentsVectorStore(); 57 | 58 | // Delete all earnings documents for a fresh start 59 | await documents.removeAll('forecast'); 60 | const docs = []; 61 | for (const stock of stocks) { 62 | const { symbol, longname: name } = stock; 63 | const lastEarnings = await documents.getLatestEarningReport(symbol); 64 | const forecast = await generateForecast({ 65 | symbol, 66 | name, 67 | summary: stock.long_business_summary, 68 | lastEarning: lastEarnings.content 69 | }); 70 | const document: Document = { 71 | metadata: { 72 | id: generateId(), 73 | title: `Forecast for ${symbol}`, 74 | symbol, 75 | type: 'forecast', 76 | }, 77 | pageContent: forecast, 78 | }; 79 | docs.push(document); 80 | } 81 | 82 | await vectorStore.addDocuments(docs); 83 | 84 | process.exit(0); 85 | } 86 | 87 | main().catch((err) => { 88 | console.error("Found error inserting Documents on Vector Store"); 89 | console.error(err); 90 | }); 91 | -------------------------------------------------------------------------------- /app/actions.tsx: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { generateId } from "ai"; 4 | import { createAI, getAIState } from "ai/rsc"; 5 | 6 | import { conversations } from "@/lib/db"; 7 | import { confirmPurchase } from "@/llm/actions/confirm-purchase"; 8 | import { continueConversation } from "@/llm/actions/continue-conversation"; 9 | import { checkEnrollment } from "@/llm/actions/newsletter"; 10 | import * as serialization from "@/llm/components/serialization"; 11 | import { ClientMessage, ServerMessage } from "@/llm/types"; 12 | import { getUser as fetchUser } from "@/sdk/auth0/mgmt"; 13 | import { getUser } from "@/sdk/fga"; 14 | 15 | type Props = Parameters>>[0] & { 16 | conversationID: string; 17 | readOnly: boolean; 18 | }; 19 | 20 | const HIDDEN_ROLES = ["system", "tool"]; 21 | 22 | export const AI = (p: Props) => { 23 | const { conversationID, readOnly, ...params } = p; 24 | 25 | const AIContext = createAI({ 26 | actions: { 27 | continueConversation, 28 | confirmPurchase, 29 | checkEnrollment, 30 | }, 31 | onSetAIState: async ({ state }) => { 32 | "use server"; 33 | const user = await getUser(); 34 | await conversations.save({ 35 | id: conversationID, 36 | messages: state, 37 | ownerID: user.sub, 38 | }); 39 | }, 40 | // @ts-ignore 41 | onGetUIState: async () => { 42 | "use server"; 43 | 44 | const history: readonly ServerMessage[] = getAIState() as readonly ServerMessage[]; 45 | 46 | return history 47 | .filter((c) => { 48 | const isHidden = c.hidden; 49 | const isToolCall = 50 | c.role === "assistant" && Array.isArray(c.content) && c.content.some((c) => c.type === "tool-call"); 51 | return !isHidden && !isToolCall && !HIDDEN_ROLES.includes(c.role); 52 | }) 53 | .map(({ role, content, componentName, params }) => { 54 | if (componentName) { 55 | const Component = serialization.components[componentName]; 56 | if (Component) { 57 | return { 58 | id: generateId(), 59 | role, 60 | display: ( 61 | // @ts-ignore 62 | // this one is complicated to fix, but it's not a big deal 63 | [0])} readOnly={readOnly} /> 64 | ), 65 | }; 66 | } 67 | } 68 | 69 | return { 70 | id: generateId(), 71 | role, 72 | display: content, 73 | }; 74 | }); 75 | }, 76 | }); 77 | 78 | return ; 79 | }; 80 | 81 | export const fetchUserById = async (user_id: string) => { 82 | const { data: user } = await fetchUser(user_id); 83 | return user; 84 | }; 85 | -------------------------------------------------------------------------------- /llm/ai-helpers.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import { z, ZodTypeAny } from "zod"; 3 | 4 | // Copied from vercel/ai because these types are not exported 5 | type Streamable$1 = ReactNode | Promise; 6 | export type Renderer$1> = (...args: T) => Streamable$1 | Generator | AsyncGenerator; 7 | export type RenderTool = { 8 | description?: string; 9 | parameters: T, 10 | generate?: Renderer$1<[ 11 | z.infer, 12 | { 13 | toolName: string; 14 | toolCallId: string; 15 | } 16 | ]>; 17 | }; 18 | 19 | /** 20 | * 21 | * Defines a tool with a name and a set of parameters 22 | * 23 | * @param name - The name of the tool 24 | * @param params - The parameters of the tool 25 | * @returns 26 | */ 27 | export const defineTool = ( 28 | name: TName, 29 | params: RenderTool | (() => RenderTool | Promise>) 30 | ): { [K in TName]: () => Promise> } => { 31 | let tool: () => Promise>; 32 | if (typeof params === "function") { 33 | tool = () => Promise.resolve(params()); 34 | } else { 35 | tool = () => Promise.resolve(params); 36 | } 37 | return { [name]: tool } as { [K in TName]: () => Promise> }; 38 | }; 39 | 40 | // Helper types for the composeTools function 41 | type Unpromisify = T extends Promise ? U : T; 42 | type UnionToIntersection = 43 | (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never; 44 | type ExtractReturnTypes = { 45 | [K in keyof T]: T[K] extends { [P in keyof T[K]]: () => infer R } ? { [P in keyof T[K]]: Unpromisify } : never 46 | }; 47 | type MergeReturnTypes = UnionToIntersection[number]>; 48 | 49 | /** 50 | * 51 | * Helper function to combine tools into a tool object. 52 | * 53 | * @param tools - The tools to combine 54 | * @returns The combined tools 55 | */ 56 | export async function composeTools< 57 | TOOLS extends { [name: string]: z.ZodTypeAny } = {}, 58 | TOOL_GENERATORS extends { [K in keyof TOOLS]: () => Promise> }[] = [] 59 | >(...toolGenerators: TOOL_GENERATORS): Promise> { 60 | return Promise.all( 61 | toolGenerators.map(async generator => { 62 | const tools = await Promise.all( 63 | Object.entries(generator) 64 | .map(async ([toolName, toolGenerator]) => { 65 | const tool = await toolGenerator(); 66 | return { [toolName]: tool } as { [K in keyof TOOLS]: RenderTool }; 67 | }) 68 | ); 69 | return Object.assign({}, ...tools); 70 | }) 71 | ).then((resolvedTools) => { 72 | return Object.assign({}, ...resolvedTools) as MergeReturnTypes; 73 | }); 74 | } 75 | -------------------------------------------------------------------------------- /sdk/fga/langchain/rag.ts: -------------------------------------------------------------------------------- 1 | import { Document, DocumentInterface } from "@langchain/core/documents"; 2 | import { BaseRetriever, BaseRetrieverInput } from "@langchain/core/retrievers"; 3 | import { ClientCheckRequest as FGACheckRequest, ConsistencyPreference, OpenFgaClient } from "@openfga/sdk"; 4 | 5 | import type { CallbackManagerForRetrieverRun } from "@langchain/core/callbacks/manager"; 6 | 7 | type CheckDocument = ( 8 | doc: DocumentInterface, 9 | query: string 10 | ) => FGACheckRequest; 11 | 12 | type FGARetrieverArgs = { 13 | 14 | /** 15 | * The retriever to wrap. 16 | */ 17 | retriever: BaseRetriever; 18 | 19 | /** 20 | * The FGA client to use for checking permissions. 21 | */ 22 | fgaClient: OpenFgaClient; 23 | 24 | fields?: BaseRetrieverInput; 25 | 26 | /** 27 | * Optional function to create the check permission tuple. 28 | */ 29 | buildQuery: CheckDocument; 30 | }; 31 | 32 | export class FGARetriever extends BaseRetriever { 33 | lc_namespace = ["langchain", "retrievers"]; 34 | 35 | private retriever: BaseRetriever; 36 | private checkDocument: CheckDocument; 37 | private fgaClient: OpenFgaClient; 38 | 39 | constructor(params: FGARetrieverArgs) { 40 | super(params.fields); 41 | this.fgaClient = params.fgaClient; 42 | this.checkDocument = params.buildQuery; 43 | this.retriever = params.retriever; 44 | } 45 | 46 | static adaptFGA( 47 | args: FGARetrieverArgs 48 | ): FGARetriever { 49 | return new FGARetriever({ ...args }); 50 | } 51 | 52 | async _getRelevantDocuments( 53 | query: string, 54 | runManager?: CallbackManagerForRetrieverRun 55 | ): Promise { 56 | const documents = await this.retriever._getRelevantDocuments( 57 | query, 58 | runManager 59 | ); 60 | 61 | const out = documents.reduce( 62 | (out, doc) => { 63 | const check = this.checkDocument(doc, query); 64 | out.checks.push(check); 65 | out.documentToObject.set(doc, check.object); 66 | return out; 67 | }, 68 | { 69 | checks: [] as FGACheckRequest[], 70 | documentToObject: new Map< 71 | DocumentInterface, 72 | string 73 | >(), 74 | } 75 | ); 76 | 77 | const { checks, documentToObject } = out; 78 | const resultsByObject = await this.accessByDocument(checks); 79 | 80 | return documents.filter( 81 | (d) => resultsByObject.get(documentToObject.get(d)!) ?? false 82 | ); 83 | } 84 | 85 | private async accessByDocument( 86 | checks: FGACheckRequest[] 87 | ): Promise> { 88 | const results = await this.fgaClient.batchCheck(checks, { 89 | consistency: ConsistencyPreference.HigherConsistency 90 | }); 91 | return results.responses.reduce((c: Map, v) => { 92 | c.set(v._request.object, v.allowed || false); 93 | return c; 94 | }, new Map()); 95 | } 96 | } 97 | 98 | 99 | -------------------------------------------------------------------------------- /components/chat/share/users-permissions-list.tsx: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import React from "react"; 4 | 5 | import { fetchUserById } from "@/app/actions"; 6 | import { getChatUsers } from "@/app/chat/[id]/actions"; 7 | import { getAvatarFallback } from "@/components/auth0/user-button"; 8 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 9 | import { ScrollArea } from "@/components/ui/scroll-area"; 10 | import { ChatUser } from "@/lib/db/chat-users"; 11 | 12 | import { UserPermissionActions } from "./user-permission-actions"; 13 | 14 | import type { Claims } from "@auth0/nextjs-auth0"; 15 | export interface ShareConversationProps { 16 | user: Claims; 17 | chatId: string | undefined; 18 | } 19 | 20 | export interface ChatUsersPermissionsList { 21 | chatId: string | undefined; 22 | } 23 | 24 | interface UserPermissionBlockProps { 25 | user: ChatUser; 26 | } 27 | 28 | export async function UserPermissionBlock({ user }: UserPermissionBlockProps) { 29 | const { email } = user; 30 | 31 | // Default user profile stored chat_users table 32 | let userProfile = { 33 | picture: "", 34 | name: email, 35 | given_name: email.split("")[0], 36 | family_name: email.split("")[1], 37 | }; 38 | 39 | if (user.user_id) { 40 | // embed user profile 41 | const userInfo = await fetchUserById(user.user_id); 42 | userProfile = { ...userProfile, ...userInfo }; 43 | } 44 | 45 | const picture = userProfile?.picture; 46 | const name = userProfile?.name; 47 | const given_name = userProfile?.given_name; 48 | const family_name = userProfile?.family_name; 49 | 50 | return ( 51 |
  • 52 |
    53 | 54 | 55 | {getAvatarFallback({ family_name, given_name })} 56 | 57 |
    58 | {name} 59 | {email} 60 |
    61 |
    62 | 63 | 64 |
  • 65 | ); 66 | } 67 | 68 | export async function ChatUsersPermissionsList({ chatId }: ChatUsersPermissionsList) { 69 | // render nothing if we are not in the context of a chat 70 | if (!chatId) { 71 | return null; 72 | } 73 | 74 | const viewers = await getChatUsers(chatId!); 75 | 76 | return ( 77 |
    78 |

    Who has access

    79 | 80 |
      81 | {viewers.map((user) => ( 82 | 83 | ))} 84 |
    85 |
    86 |
    87 | ); 88 | } 89 | -------------------------------------------------------------------------------- /components/chat/share/user-permission-actions.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | 5 | import { removeChatReader } from "@/app/chat/[id]/actions"; 6 | import { CheckIcon, ChevronDownIcon, TrashIcon } from "@/components/icons"; 7 | import { Button } from "@/components/ui/button"; 8 | import { 9 | DropdownMenu, 10 | DropdownMenuContent, 11 | DropdownMenuItem, 12 | DropdownMenuSeparator, 13 | DropdownMenuTrigger, 14 | } from "@/components/ui/dropdown-menu"; 15 | import { toast } from "@/components/ui/use-toast"; 16 | import { ChatUser } from "@/lib/db/chat-users"; 17 | import { cn } from "@/lib/utils"; 18 | 19 | export interface UserPermissionActionsProps { 20 | user: ChatUser; 21 | } 22 | 23 | export function UserPermissionActions({ user }: UserPermissionActionsProps) { 24 | const { id, chat_id } = user; 25 | const role = user.access === "can_view" ? "viewer" : "owner"; 26 | 27 | async function handleOnRemove() { 28 | try { 29 | await removeChatReader(chat_id, id); 30 | } catch (err) { 31 | toast({ 32 | title: "Error!", 33 | description: (err as Error).message || "There was a problem removing access to this chat. Try again later.", 34 | variant: "destructive", 35 | }); 36 | } 37 | } 38 | 39 | return ( 40 | <> 41 | {role === "owner" && ( 42 | 45 | )} 46 | {role === "viewer" && ( 47 | 48 | 49 | 53 | 54 | 55 | 56 | 59 | 60 | 61 | 62 | 69 | 70 | 71 | 72 | 73 | 80 | 81 | 82 | 83 | )} 84 | 85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /scripts/llm/insert-earnings.ts: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | import type { Document } from "@langchain/core/documents"; 3 | import stocks from "@/lib/market/stocks.json"; 4 | 5 | dotenv.config({ path: ".env.local" }); 6 | 7 | import { getDocumentsVectorStore } from "../../lib/documents"; 8 | import { documents } from "../../lib/db"; 9 | import { generateId, generateText } from "ai"; 10 | import { openai } from "@ai-sdk/openai"; 11 | 12 | /** 13 | * The purpose of this file is to generate earnings reports for the last 4 14 | * quarters for each stock in the stocks.json file. 15 | * 16 | * We use the OpenAI API to generate the earnings reports. 17 | */ 18 | 19 | const generateEarningsReport = async ({ 20 | symbol, 21 | name, 22 | summary, 23 | quarter, 24 | previousReports, 25 | situation 26 | }: { summary: string, name: string, symbol: string, quarter: string, previousReports: string[], situation: string }) => { 27 | const { text } = await generateText({ 28 | model: openai("gpt-4o"), 29 | temperature: 0.2, 30 | system: ` 31 | You represent the board of directors of a fictitious company called ${name} with ticker ${symbol}. 32 | 33 | The company summary is: ${summary}. 34 | 35 | You are writing the earning report - SEC filling for the ${quarter}. 36 | 37 | The overall situation is ${situation}. 38 | 39 | ${previousReports.length > 0 ? 40 | 'The previous earnings reports were:' : ''} 41 | 42 | ${previousReports.join('\n')} 43 | `, 44 | prompt: `Generate the earnings report for quarter ${quarter} for ${name}.`, 45 | }); 46 | return text; 47 | } 48 | 49 | async function main() { 50 | const vectorStore = await getDocumentsVectorStore(); 51 | 52 | // Delete all earnings documents for a fresh start 53 | await documents.removeAll('earning'); 54 | 55 | const docs = []; 56 | 57 | const last4Quarters = [ 58 | '4th Quarter 2023', 59 | '1st Quarter 2024', 60 | '2nd Quarter 2024', 61 | '3rd Quarter 2024', 62 | ]; 63 | 64 | for (const stock of stocks) { 65 | const { symbol } = stock; 66 | const previousReports: string[] = []; 67 | for (const quarter of last4Quarters) { 68 | const earningReport = await generateEarningsReport({ 69 | ...stock, 70 | name: stock.longname, 71 | summary: stock.long_business_summary, 72 | quarter, 73 | previousReports 74 | }); 75 | 76 | previousReports.push(earningReport); 77 | 78 | const document: Document = { 79 | metadata: { 80 | id: generateId(), 81 | title: `${quarter} earnings report for ${symbol}`, 82 | symbol: symbol, 83 | type: 'earning', 84 | }, 85 | pageContent: earningReport, 86 | }; 87 | 88 | docs.push(document); 89 | } 90 | } 91 | 92 | await vectorStore.addDocuments(docs); 93 | process.exit(0); 94 | } 95 | 96 | main().catch((err) => { 97 | console.error("Found error inserting Documents on Vector Store"); 98 | console.error(err); 99 | }); 100 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Auth0 projects 2 | 3 | A big welcome and thank you for considering contributing to the Auth0 open source projects. It’s people like you that make it a reality for users in our community. 4 | 5 | Reading and following these guidelines will help us make the contribution process easy and effective for everyone involved. It also communicates that you agree to respect the time of the developers managing and developing these open source projects. In return, we will reciprocate that respect by addressing your issue, assessing changes, and helping you finalize your pull requests. 6 | 7 | ### Quicklinks 8 | 9 | - [Code of Conduct](#code-of-conduct) 10 | - [Getting Started](#getting-started) 11 | - [Making Changes](#making-changes) 12 | - [Opening Issues](#opening-issues) 13 | - [Submitting Pull Requests](#submitting-pull-requests) 14 | - [Getting in Touch](#getting-in-touch) 15 | - [Got a question or problem?](#got-a-question-or-problem?) 16 | - [Vulnerability Reporting](#vulnerability-reporting) 17 | 18 | ## Code of Conduct 19 | 20 | By participating and contributing to this project, you are expected to uphold our [Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md). 21 | 22 | ## Getting Started 23 | 24 | ### Making Changes 25 | 26 | When contributing to a repository, the first step is to open an issue in that repository to discuss the change you wish to make before making them. 27 | 28 | ### Opening Issues 29 | 30 | Before you submit a new issue please make sure to search all open and closed issues. It is possible your feature request/issue has already been answered. 31 | 32 | This repo includes an issue template that will walk through all the places to check before submitting your issue here. Please follow the instructions there to make sure this is not a duplicate issue and we have everything we need to research and reproduce this problem. 33 | 34 | ### Submitting Pull Requests 35 | 36 | Same goes for PRs, please search all open and closed PRs before submitting a new one. We do not want duplicate effort. 37 | 38 | In general, we follow the "fork-and-pull" Git workflow. 39 | 40 | - Fork the repository to your own Github account 41 | - Clone the project to your machine 42 | - Create a branch locally with a succinct but descriptive name 43 | - Commit changes to the branch 44 | - Push changes to your fork 45 | - Open a Pull Request in the repository (not your own fork) and follow the PR template so that we can efficiently review the changes. 46 | 47 | NOTE: Be sure to merge the latest from "upstream" before making a pull request. 48 | 49 | ## Getting in touch 50 | 51 | ### Have a question or problem? 52 | 53 | Please do not open issues for general support or usage questions. Instead, join us over in the Auth0Lab community at [discord.gg/XbQpZSF2Ys](https://discord.gg/XbQpZSF2Ys) and post your question there in the correct channel. 54 | 55 | ### Vulnerability Reporting 56 | 57 | Please do not report security vulnerabilities on the public GitHub issue tracker. The [Responsible Disclosure Program](https://auth0.com/whitehat) details the procedure for disclosing security issues. 58 | -------------------------------------------------------------------------------- /components/ui/drawer.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { Drawer as DrawerPrimitive } from "vaul"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Drawer = ({ shouldScaleBackground = true, ...props }: React.ComponentProps) => ( 9 | 10 | ); 11 | Drawer.displayName = "Drawer"; 12 | 13 | const DrawerTrigger = DrawerPrimitive.Trigger; 14 | 15 | const DrawerPortal = DrawerPrimitive.Portal; 16 | 17 | const DrawerClose = DrawerPrimitive.Close; 18 | 19 | const DrawerOverlay = React.forwardRef< 20 | React.ElementRef, 21 | React.ComponentPropsWithoutRef 22 | >(({ className, ...props }, ref) => ( 23 | 24 | )); 25 | DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName; 26 | 27 | const DrawerContent = React.forwardRef< 28 | React.ElementRef, 29 | React.ComponentPropsWithoutRef 30 | >(({ className, children, ...props }, ref) => ( 31 | 32 | 33 | 41 | {/*
    */} 42 | {children} 43 | 44 | 45 | )); 46 | DrawerContent.displayName = "DrawerContent"; 47 | 48 | const DrawerHeader = ({ className, ...props }: React.HTMLAttributes) => ( 49 |
    50 | ); 51 | DrawerHeader.displayName = "DrawerHeader"; 52 | 53 | const DrawerFooter = ({ className, ...props }: React.HTMLAttributes) => ( 54 |
    55 | ); 56 | DrawerFooter.displayName = "DrawerFooter"; 57 | 58 | const DrawerTitle = React.forwardRef< 59 | React.ElementRef, 60 | React.ComponentPropsWithoutRef 61 | >(({ className, ...props }, ref) => ( 62 | 67 | )); 68 | DrawerTitle.displayName = DrawerPrimitive.Title.displayName; 69 | 70 | const DrawerDescription = React.forwardRef< 71 | React.ElementRef, 72 | React.ComponentPropsWithoutRef 73 | >(({ className, ...props }, ref) => ( 74 | 75 | )); 76 | DrawerDescription.displayName = DrawerPrimitive.Description.displayName; 77 | 78 | export { 79 | Drawer, 80 | DrawerPortal, 81 | DrawerOverlay, 82 | DrawerTrigger, 83 | DrawerClose, 84 | DrawerContent, 85 | DrawerHeader, 86 | DrawerFooter, 87 | DrawerTitle, 88 | DrawerDescription, 89 | }; 90 | -------------------------------------------------------------------------------- /components/fga/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | import { SimplePlusIcon } from "../icons"; 6 | import { Button } from "../ui/button"; 7 | 8 | const DottedContainer = ({ children }: { children: React.ReactNode }) => ( 9 |
    {children}
    10 | ); 11 | 12 | const UnautorizedIcon = () => { 13 | return ( 14 | 15 | 23 | 24 | 25 | 26 | ); 27 | }; 28 | 29 | export function ErrorContainer({ 30 | title, 31 | message, 32 | icon, 33 | action, 34 | }: { 35 | title?: string; 36 | message?: string; 37 | icon?: React.ReactNode; 38 | action?: { 39 | label: string; 40 | onClick: () => void; 41 | icon?: React.ReactNode; 42 | className?: string; 43 | }; 44 | }) { 45 | const finalAction = 46 | action !== undefined 47 | ? action 48 | : { 49 | label: "Create New Chat", 50 | onClick: () => (window.location.href = "/"), 51 | icon: , 52 | className: "", 53 | }; 54 | return ( 55 |
    59 | 60 |
    61 |
    62 |
    {icon || }
    63 |

    {title || "Not Authorized"}

    64 |

    65 | {message || "You are not authorized to access the requested information."} 66 |

    67 |
    68 | {finalAction && ( 69 | 77 | )} 78 |
    79 |
    80 |
    81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /components/auth0/user-button.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 2 | import { Button } from "@/components/ui/button"; 3 | import { 4 | DropdownMenu, 5 | DropdownMenuContent, 6 | DropdownMenuItem, 7 | DropdownMenuLabel, 8 | DropdownMenuSeparator, 9 | DropdownMenuTrigger, 10 | } from "@/components/ui/dropdown-menu"; 11 | 12 | interface KeyValueMap { 13 | [key: string]: any; 14 | } 15 | 16 | function LogOut() { 17 | return ( 18 | 29 | 30 | 31 | 32 | 33 | ); 34 | } 35 | 36 | export function getAvatarFallback(user: KeyValueMap) { 37 | const givenName = user.given_name; 38 | const familyName = user.family_name; 39 | const nickname = user.nickname; 40 | const name = user.name; 41 | 42 | if (givenName && familyName) { 43 | return `${givenName[0]}${familyName[0]}`; 44 | } 45 | 46 | if (nickname) { 47 | return nickname[0]; 48 | } 49 | 50 | return name[0]; 51 | } 52 | 53 | export default function UserButton({ 54 | user, 55 | children, 56 | logoutUrl = "/api/auth/logout", 57 | }: { 58 | user: KeyValueMap; 59 | children?: React.ReactNode; 60 | logoutUrl?: string; 61 | }) { 62 | const picture = user.picture; 63 | const name = user.name; 64 | const email = user.email; 65 | const resolvedLogoutUrl = logoutUrl; 66 | 67 | return ( 68 | 69 | 70 | 76 | 77 | 78 | 79 |
    80 | 81 | 82 | {getAvatarFallback(user)} 83 | 84 |
    85 |

    {name}

    86 |

    87 | {email} 88 |

    89 |
    90 |
    91 |
    92 | 93 | {children && ( 94 | <> 95 | 96 | {children} 97 | 98 | 99 | )} 100 | 101 | 102 | 103 | 104 | Log out 105 | 106 | 107 |
    108 |
    109 | ); 110 | } 111 | -------------------------------------------------------------------------------- /lib/db/conditional-purchases.ts: -------------------------------------------------------------------------------- 1 | import { sql } from "./sql"; 2 | 3 | export type ConditionalPurchaseStatus = "pending" | "completed" | "canceled"; 4 | 5 | export type ConditionalPurchase = { 6 | id: string; 7 | user_id: string; 8 | status: ConditionalPurchaseStatus; 9 | symbol: string; 10 | quantity: number; 11 | metric: string; 12 | operator: string; 13 | threshold: number; 14 | created_at: Date; 15 | updated_at: Date; 16 | link?: string; 17 | }; 18 | 19 | const mapConditionalPurchaseFromDB = ( 20 | conditionalPurchase: any 21 | ): ConditionalPurchase => ({ 22 | ...conditionalPurchase, 23 | threshold: parseFloat(conditionalPurchase.threshold), 24 | created_at: new Date(conditionalPurchase.created_at), 25 | updated_at: new Date(conditionalPurchase.updated_at), 26 | }); 27 | 28 | export const create = async ({ 29 | user_id, 30 | symbol, 31 | quantity, 32 | metric, 33 | operator, 34 | threshold, 35 | status, 36 | link, 37 | }: { 38 | user_id: string; 39 | symbol: string; 40 | quantity: number; 41 | metric: string; 42 | operator: string; 43 | threshold: number; 44 | status: ConditionalPurchaseStatus; 45 | link?: string; 46 | }): Promise => { 47 | const result = await sql` 48 | INSERT INTO conditional_purchases (user_id, symbol, quantity, metric, operator, threshold, status, link) 49 | VALUES (${user_id}, ${symbol.toUpperCase()}, ${quantity}, ${metric.toUpperCase()}, ${operator}, ${threshold}, ${status}, ${ 50 | link ?? null 51 | }) 52 | RETURNING * 53 | `; 54 | 55 | return mapConditionalPurchaseFromDB(result[0]); 56 | }; 57 | 58 | export const update = async ( 59 | id: string, 60 | params: Omit< 61 | Partial, 62 | | "id" 63 | | "user_id" 64 | | "created_at" 65 | | "updated_at" 66 | | "symbol" 67 | | "quantity" 68 | | "metric" 69 | | "operator" 70 | | "threshold" 71 | > 72 | ) => { 73 | const result = await sql` 74 | UPDATE conditional_purchases 75 | SET status = COALESCE(${params.status ?? null}, status), 76 | link = COALESCE(${params.link ?? null}, link) 77 | WHERE id = ${id} 78 | RETURNING * 79 | `; 80 | 81 | return result.length > 0 ? mapConditionalPurchaseFromDB(result[0]) : null; 82 | }; 83 | 84 | export const getByID = async ( 85 | user_id: string, 86 | id: string 87 | ): Promise => { 88 | const result = await sql` 89 | SELECT * FROM conditional_purchases 90 | WHERE user_id = ${user_id} AND id = ${id} 91 | `; 92 | 93 | return result.length > 0 ? mapConditionalPurchaseFromDB(result[0]) : null; 94 | }; 95 | 96 | export const getMatchingPurchases = async ( 97 | symbol: string, 98 | metric: string, 99 | value: number, 100 | user_id?: string 101 | ): Promise => { 102 | let query = sql` 103 | SELECT * FROM conditional_purchases 104 | WHERE symbol = ${symbol.toUpperCase()} AND metric = ${metric.toUpperCase()} AND status = 'pending' AND ( 105 | (operator = '=' AND ${value} = threshold) OR 106 | (operator = '>' AND ${value} > threshold) OR 107 | (operator = '>=' AND ${value} >= threshold) OR 108 | (operator = '<' AND ${value} < threshold) OR 109 | (operator = '<=' AND ${value} <= threshold) 110 | ) 111 | `; 112 | 113 | if (user_id) { 114 | query = sql`${query} AND user_id = ${user_id}`; 115 | } 116 | 117 | const result = await query; 118 | return result.map(mapConditionalPurchaseFromDB); 119 | }; 120 | -------------------------------------------------------------------------------- /sdk/auth0/3rd-party-apis/index.tsx: -------------------------------------------------------------------------------- 1 | import { getGoogleConnectionName } from "@/lib/utils"; 2 | import { getSession } from "@auth0/nextjs-auth0"; 3 | 4 | import * as box from "./providers/box"; 5 | import * as google from "./providers/google"; 6 | 7 | const PROVIDERS_APIS = [ 8 | { 9 | name: "google", 10 | api: "google-calendar", 11 | requiredScopes: ["https://www.googleapis.com/auth/calendar.freebusy"], 12 | }, 13 | { 14 | name: "google", 15 | api: "google-all", 16 | requiredScopes: ["https://www.googleapis.com/auth/calendar.freebusy"], 17 | }, 18 | { 19 | name: "box", 20 | api: "box-write", 21 | requiredScopes: ["root_readwrite"], 22 | }, 23 | ]; 24 | 25 | type ProviderContext = { 26 | isAPIAccessEnabled: boolean; 27 | containsRequiredScopes: boolean; 28 | }; 29 | 30 | export type Context = { 31 | google: ProviderContext; 32 | box: ProviderContext; 33 | }; 34 | 35 | type AvailableProviders = "google" | "box"; 36 | 37 | export type Provider = { 38 | name: AvailableProviders; 39 | api: string; 40 | requiredScopes: string[]; 41 | }; 42 | 43 | type With3PartyApisParams = { 44 | providers: Provider[]; 45 | }; 46 | 47 | const providerMapper = { 48 | google: async (requiredScopes: string[]) => { 49 | const provider: ProviderContext = { 50 | isAPIAccessEnabled: false, 51 | containsRequiredScopes: false, 52 | }; 53 | 54 | const accessToken = await google.getAccessToken(); 55 | 56 | if (accessToken) { 57 | provider.containsRequiredScopes = await google.verifyAccessToken(accessToken, requiredScopes); 58 | } 59 | 60 | provider.isAPIAccessEnabled = !!accessToken; 61 | return provider; 62 | }, 63 | box: async () => { 64 | const provider: ProviderContext = { 65 | isAPIAccessEnabled: false, 66 | containsRequiredScopes: false, 67 | }; 68 | 69 | const accessToken = await box.getAccessToken(); 70 | if (accessToken) { 71 | provider.isAPIAccessEnabled = true; 72 | provider.containsRequiredScopes = true; // TODO: find a way to validate required scopes 73 | } 74 | 75 | return provider; 76 | }, 77 | }; 78 | 79 | export async function getThirdPartyContext(params: With3PartyApisParams) { 80 | const context: Context = { 81 | google: { 82 | isAPIAccessEnabled: false, 83 | containsRequiredScopes: false, 84 | }, 85 | box: { 86 | isAPIAccessEnabled: false, 87 | containsRequiredScopes: false, 88 | }, 89 | }; 90 | 91 | for (const provider of params.providers) { 92 | context[provider.name] = await providerMapper[provider.name](provider.requiredScopes); 93 | } 94 | 95 | return context; 96 | } 97 | 98 | export async function handle3rdPartyParams(thirdPartyApi: string) { 99 | const session = await getSession(); 100 | const user = session?.user; 101 | let authorizationParams = {}; 102 | 103 | const provider = PROVIDERS_APIS.find((provider) => provider.api === thirdPartyApi || provider.name === thirdPartyApi); 104 | 105 | switch (provider?.name) { 106 | case "google": 107 | authorizationParams = { 108 | connection: getGoogleConnectionName(), 109 | connection_scope: provider?.requiredScopes.join(), 110 | access_type: "offline", 111 | login_hint: user?.email, 112 | }; 113 | break; 114 | case "box": 115 | authorizationParams = { 116 | connection: "box", 117 | connection_scope: provider?.requiredScopes.join(), 118 | access_type: "offline", 119 | login_hint: user?.email, 120 | }; 121 | break; 122 | } 123 | 124 | return authorizationParams; 125 | } 126 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "market0", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "fga:migrate:create": "ts-node ./scripts/fga/generate.ts", 11 | "fga:sync-docs": "ts-node ./scripts/fga/syncDocs.ts", 12 | "llm:earnings:insert": "ts-node ./scripts/llm/insert-earnings.ts", 13 | "llm:forecasts:insert": "ts-node ./scripts/llm/insert-forecasts.ts", 14 | "db:up": "docker compose --env-file .env.local up pg schemas -d", 15 | "db:down": "docker compose --env-file .env.local down -v", 16 | "db:connect": "docker compose --env-file .env.local exec pg sh -c 'psql -U $POSTGRES_USER -d $POSTGRES_DB'", 17 | "migration:generate": "flywayfg --directory database/market0 --padding 3 --title", 18 | "migration:up": "docker compose --env-file .env.local up schemas" 19 | }, 20 | "license": "Apache-2.0", 21 | "dependencies": { 22 | "@ai-sdk/amazon-bedrock": "^0.0.29", 23 | "@ai-sdk/mistral": "^0.0.41", 24 | "@ai-sdk/openai": "^0.0.50", 25 | "@auth0/nextjs-auth0": "^3.5.0", 26 | "@hookform/resolvers": "^3.9.0", 27 | "@langchain/community": "^0.2.31", 28 | "@langchain/openai": "^0.2.7", 29 | "@mdx-js/loader": "^3.0.1", 30 | "@mdx-js/react": "^3.0.1", 31 | "@neondatabase/serverless": "^0.9.4", 32 | "@next/mdx": "^14.2.15", 33 | "@openfga/sdk": "^0.6.2", 34 | "@radix-ui/react-avatar": "^1.1.1", 35 | "@radix-ui/react-dialog": "^1.1.2", 36 | "@radix-ui/react-dropdown-menu": "^2.1.1", 37 | "@radix-ui/react-icons": "^1.3.0", 38 | "@radix-ui/react-label": "^2.1.0", 39 | "@radix-ui/react-popover": "^1.1.2", 40 | "@radix-ui/react-scroll-area": "^1.1.0", 41 | "@radix-ui/react-select": "^2.1.2", 42 | "@radix-ui/react-separator": "^1.1.0", 43 | "@radix-ui/react-slot": "^1.1.0", 44 | "@radix-ui/react-toast": "^1.2.1", 45 | "@radix-ui/react-tooltip": "^1.1.3", 46 | "@sentry/nextjs": "^8.37.1", 47 | "@types/mdx": "^2.0.13", 48 | "ai": "^3.3.12", 49 | "auth0": "^4.10.0", 50 | "class-variance-authority": "^0.7.0", 51 | "clsx": "^2.1.1", 52 | "cmdk": "^1.0.0", 53 | "d3-scale": "^4.0.2", 54 | "date-fns": "^3.6.0", 55 | "dotenv": "^16.4.5", 56 | "jose": "^5.8.0", 57 | "langchain": "^0.2.17", 58 | "lodash-es": "^4.17.21", 59 | "lru-cache": "^11.0.1", 60 | "lucide-react": "^0.441.0", 61 | "next": "^14.2.12", 62 | "next-themes": "^0.3.0", 63 | "pg": "^8.12.0", 64 | "postgres": "^3.4.4", 65 | "react": "^18", 66 | "react-device-detect": "^2.2.3", 67 | "react-dom": "^18", 68 | "react-hook-form": "^7.53.0", 69 | "react-markdown": "^9.0.1", 70 | "react-syntax-highlighter": "^15.6.1", 71 | "remark-gfm": "^4.0.0", 72 | "tailwind-merge": "^2.5.2", 73 | "usehooks-ts": "^3.1.0", 74 | "vaul": "^1.0.0", 75 | "yahoo-finance2": "^2.12.2", 76 | "zod": "^3.23.8" 77 | }, 78 | "devDependencies": { 79 | "@types/d3-scale": "^4.0.8", 80 | "@types/lodash-es": "^4.17.12", 81 | "@types/node": "^20", 82 | "@types/pg": "^8.11.6", 83 | "@types/react": "^18", 84 | "@types/react-dom": "^18", 85 | "@types/react-syntax-highlighter": "^15.5.13", 86 | "eslint": "^8", 87 | "eslint-config-next": "14.1.0", 88 | "flywayfg": "^1.0.1", 89 | "postcss": "^8", 90 | "puppeteer": "^23.3.1", 91 | "tailwindcss": "3.4.1", 92 | "ts-node": "^10.9.2", 93 | "tsconfig-paths": "^4.2.0", 94 | "turndown": "^7.2.0", 95 | "typescript": "^5" 96 | }, 97 | "prettier": { 98 | "printWidth": 120 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config = { 4 | darkMode: ["class"], 5 | content: [ 6 | "./pages/**/*.{ts,tsx}", 7 | "./components/**/*.{ts,tsx}", 8 | "./app/**/*.{ts,tsx}", 9 | "./src/**/*.{ts,tsx}", 10 | "./llm/components/**/*.{ts,tsx,mdx}", 11 | "./sdk/**/*.{ts,tsx}", 12 | "./mdx-components.tsx", 13 | ], 14 | prefix: "", 15 | theme: { 16 | container: { 17 | center: true, 18 | padding: "2rem", 19 | screens: { 20 | "2xl": "1400px", 21 | }, 22 | }, 23 | 24 | extend: { 25 | backgroundImage: { 26 | "text-gradient": "linear-gradient(91deg, #574ACD 57.13%, #1A163C 92.88%)", 27 | }, 28 | colors: { 29 | border: "hsl(var(--border))", 30 | input: "hsl(var(--input))", 31 | ring: "hsl(var(--ring))", 32 | background: "hsl(var(--background))", 33 | foreground: "hsl(var(--foreground))", 34 | primary: { 35 | DEFAULT: "hsl(var(--primary))", 36 | foreground: "hsl(var(--primary-foreground))", 37 | }, 38 | secondary: { 39 | DEFAULT: "hsl(var(--secondary))", 40 | foreground: "hsl(var(--secondary-foreground))", 41 | }, 42 | destructive: { 43 | DEFAULT: "hsl(var(--destructive))", 44 | foreground: "hsl(var(--destructive-foreground))", 45 | }, 46 | muted: { 47 | DEFAULT: "hsl(var(--muted))", 48 | foreground: "hsl(var(--muted-foreground))", 49 | }, 50 | accent: { 51 | DEFAULT: "hsl(var(--accent))", 52 | foreground: "hsl(var(--accent-foreground))", 53 | }, 54 | popover: { 55 | DEFAULT: "hsl(var(--popover))", 56 | foreground: "hsl(var(--popover-foreground))", 57 | }, 58 | card: { 59 | DEFAULT: "hsl(var(--card))", 60 | foreground: "hsl(var(--card-foreground))", 61 | }, 62 | }, 63 | borderRadius: { 64 | lg: "var(--radius)", 65 | md: "calc(var(--radius) - 2px)", 66 | sm: "calc(var(--radius) - 4px)", 67 | }, 68 | keyframes: { 69 | "accordion-down": { 70 | from: { height: "0" }, 71 | to: { height: "var(--radix-accordion-content-height)" }, 72 | }, 73 | "accordion-up": { 74 | from: { height: "var(--radix-accordion-content-height)" }, 75 | to: { height: "0" }, 76 | }, 77 | shimmer: { 78 | "0%, 90%, 100%": { 79 | "background-position": "calc(-100% - var(--shimmer-width)) 0", 80 | }, 81 | "30%, 60%": { 82 | "background-position": "calc(100% + var(--shimmer-width)) 0", 83 | }, 84 | }, 85 | "dots-loader": { 86 | "0%": { 87 | "box-shadow": "9px 0 rgb(87 83 78), -9px 0 #0002", 88 | background: "rgb(87 83 78)", 89 | }, 90 | "33%": { 91 | "box-shadow": "9px 0 rgb(87 83 78), -9px 0 #0002", 92 | background: "#0002", 93 | }, 94 | "66%": { 95 | "box-shadow": "9px 0 #0002, -9px 0 rgb(87 83 78)", 96 | background: "#0002", 97 | }, 98 | "100%": { 99 | "box-shadow": "9px 0 #0002, -9px 0 rgb(87 83 78)", 100 | background: "rgb(87 83 78)", 101 | }, 102 | }, 103 | }, 104 | animation: { 105 | "accordion-down": "accordion-down 0.2s ease-out", 106 | "accordion-up": "accordion-up 0.2s ease-out", 107 | "dots-loader": "dots-loader 1s infinite linear alternate;", 108 | }, 109 | }, 110 | }, 111 | } satisfies Config; 112 | 113 | export default config; 114 | -------------------------------------------------------------------------------- /app/chat/[id]/layout.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { notFound, redirect } from "next/navigation"; 3 | 4 | import { AI, fetchUserById } from "@/app/actions"; 5 | import { isChatOwner, isCurrentUserInvitedToChat, setCurrentUserAsReader } from "@/app/chat/[id]/actions"; 6 | import { ChatProvider } from "@/components/chat/context"; 7 | import ConversationPicker from "@/components/chat/conversation-picker"; 8 | import { Header } from "@/components/chat/header"; 9 | import { ShareConversation } from "@/components/chat/share"; 10 | import { ChatUsersPermissionsList } from "@/components/chat/share/users-permissions-list"; 11 | import { ErrorContainer } from "@/components/fga/error"; 12 | import { ArrowRightIcon } from "@/components/icons"; 13 | import { conversations } from "@/lib/db"; 14 | import { getUser, withFGA } from "@/sdk/fga"; 15 | import { withCheckPermission } from "@/sdk/fga/next/with-check-permission"; 16 | 17 | type RootChatParams = Readonly<{ 18 | children: React.ReactNode; 19 | params: { 20 | id: string; 21 | }; 22 | }>; 23 | 24 | async function RootLayout({ children, params }: RootChatParams) { 25 | const [conversation, isOwner, user] = await Promise.all([ 26 | conversations.get(params.id), 27 | isChatOwner(params.id), 28 | getUser(), 29 | ]); 30 | 31 | if (!conversation) { 32 | return notFound(); 33 | } 34 | 35 | if (!isOwner) { 36 | return redirect(`/read/${conversation.id}`); 37 | } 38 | 39 | const { messages, ownerID } = conversation; 40 | const ownerProfile = ownerID ? await fetchUserById(ownerID) : undefined; 41 | 42 | return ( 43 | 0} ownerProfile={ownerProfile}> 44 | 45 |
    46 |
    53 | Docs 54 | 55 | } 56 | outerElements={} 57 | > 58 | {isOwner && ( 59 | 60 | {/** 61 | * Because of a rendering bug with server components and client 62 | * components, we require passing the chatId at this instance 63 | * instead of using the chatId from the context. 64 | */} 65 | 66 | 67 | )} 68 |
    69 | {children} 70 |
    71 |
    72 |
    73 | ); 74 | } 75 | 76 | export default withCheckPermission( 77 | { 78 | checker: async ({ params }: RootChatParams) => { 79 | const chatId = params.id; 80 | const allowed = await withFGA({ 81 | object: `chat:${chatId}`, 82 | relation: "can_view", 83 | }); 84 | 85 | if (!allowed && (await isCurrentUserInvitedToChat(chatId))) { 86 | await setCurrentUserAsReader(chatId); 87 | return true; 88 | } 89 | 90 | return allowed; 91 | }, 92 | onUnauthorized: ({ params }: RootChatParams) => ( 93 | 94 |
    95 | 96 | 97 | ), 98 | }, 99 | RootLayout 100 | ); 101 | -------------------------------------------------------------------------------- /llm/components/forecasts/documents.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useActions, useUIState } from "ai/rsc"; 4 | import Link from "next/link"; 5 | import { useEffect, useState } from "react"; 6 | 7 | import { ExplanationType } from "@/components/explanation/observable"; 8 | import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; 9 | import { ClientMessage, Document } from "@/llm/types"; 10 | 11 | import { FormattedText } from "../FormattedText"; 12 | import { NotAvailableReadOnly } from "../not-available-read-only"; 13 | import { PromptUserContainer } from "../prompt-user-container"; 14 | import WarningWrapper from "../warning-wrapper"; 15 | 16 | export const Documents = ({ 17 | documents, 18 | text, 19 | symbol, 20 | finished, 21 | readOnly = false, 22 | }: { 23 | documents: Document[]; 24 | text: string; 25 | symbol: string; 26 | finished: boolean; 27 | readOnly?: boolean; 28 | }) => { 29 | const [showEnrollment, setShowEnrollment] = useState(false); 30 | 31 | const { checkEnrollment } = useActions(); 32 | const { continueConversation } = useActions(); 33 | const [, setMessages] = useUIState(); 34 | 35 | useEffect(() => { 36 | checkEnrollment({ symbol }).then((isEnrolled?: Boolean) => { 37 | setShowEnrollment(!isEnrolled); 38 | }); 39 | }, [symbol]); 40 | 41 | const enroll = () => { 42 | setShowEnrollment(false); 43 | (async () => { 44 | const response = await continueConversation({ 45 | message: `Subscribe me to the newsletter. Once done ASK me if I want to read the forecast analysis for ${symbol}.`, 46 | hidden: true, 47 | }); 48 | setMessages((prevMessages: ClientMessage[]) => [...prevMessages, response]); 49 | })(); 50 | }; 51 | 52 | return ( 53 | 54 |
    55 | 56 | {documents.length > 0 && finished && ( 57 |
    58 | {documents.map((document: Document, index: number) => ( 59 |
    60 | 61 | 62 | 63 | 69 | [{index + 1}] 70 | 71 | 72 | 73 |

    {document.metadata.title}

    74 |
    75 |
    76 |
    77 |
    78 | ))} 79 |
    80 | )} 81 | {showEnrollment && 82 | finished && 83 | (readOnly ? ( 84 | 85 | ) : ( 86 | 96 | ))} 97 |
    98 |
    99 | ); 100 | }; 101 | -------------------------------------------------------------------------------- /lib/db/chat-users.ts: -------------------------------------------------------------------------------- 1 | import { sql } from "./sql"; 2 | 3 | export type ChatUserAccess = "owner" | "can_view"; 4 | export type ChatUserStatus = "pending" | "provisioned"; 5 | 6 | export type ChatUser = { 7 | id: string; 8 | chat_id: string; 9 | email: string; 10 | user_id?: string; 11 | access: ChatUserAccess; 12 | status: ChatUserStatus; 13 | created_at: Date; 14 | updated_at: Date; 15 | }; 16 | 17 | type CreateChatUser = { 18 | chat_id: string; 19 | email: string; 20 | user_id?: string; 21 | access: ChatUserAccess; 22 | status?: ChatUserStatus; 23 | }; 24 | 25 | const mapChatUserFromDB = (chatUser: any): ChatUser => ({ 26 | ...chatUser, 27 | created_at: new Date(chatUser.created_at), 28 | updated_at: new Date(chatUser.updated_at), 29 | }); 30 | 31 | export const add = async ({ chat_id, email, user_id, access, status }: CreateChatUser): Promise => { 32 | let result; 33 | 34 | if (!user_id) { 35 | result = await sql` 36 | INSERT INTO chat_users (chat_id, email, access, status) 37 | VALUES (${chat_id}, ${email.toLowerCase()}, ${access}, ${status || "pending"}) 38 | RETURNING * 39 | `; 40 | } else { 41 | result = await sql` 42 | INSERT INTO chat_users (chat_id, email, user_id, access, status) 43 | VALUES (${chat_id}, ${email.toLowerCase()}, ${user_id}, ${access}, ${status || "pending"}) 44 | RETURNING * 45 | `; 46 | } 47 | 48 | return mapChatUserFromDB(result[0]); 49 | }; 50 | 51 | export const remove = async (id: string): Promise => { 52 | await sql` 53 | DELETE FROM chat_users 54 | WHERE id = ${id} 55 | `; 56 | }; 57 | 58 | export const get = async (id: string): Promise => { 59 | const result = await sql` 60 | SELECT * FROM chat_users 61 | WHERE id = ${id} 62 | `; 63 | 64 | return result.length > 0 ? mapChatUserFromDB(result[0]) : null; 65 | }; 66 | 67 | export const getByUserEmail = async (chat_id: string, email: string): Promise => { 68 | const result = await sql` 69 | SELECT * FROM chat_users 70 | WHERE chat_id = ${chat_id} AND email = ${email} 71 | `; 72 | 73 | return result.length > 0 ? mapChatUserFromDB(result[0]) : null; 74 | }; 75 | 76 | export const list = async (chat_id: string, { access }: { access?: ChatUserAccess } = {}): Promise => { 77 | let query = sql` 78 | SELECT * FROM chat_users 79 | WHERE chat_id = ${chat_id} 80 | `; 81 | 82 | if (access) { 83 | query = sql` 84 | SELECT * FROM chat_users 85 | WHERE chat_id = ${chat_id} 86 | AND access = ${access} 87 | `; 88 | } 89 | 90 | const result = await query; 91 | return result.map(mapChatUserFromDB); 92 | }; 93 | 94 | export const update = async ( 95 | id: string, 96 | params: { user_id?: string; access?: ChatUserAccess; status?: ChatUserStatus } 97 | ): Promise => { 98 | const result = await sql` 99 | UPDATE chat_users 100 | SET user_id = COALESCE(${params.user_id ?? null}, user_id), 101 | access = COALESCE(${params.access ?? null}, access), 102 | status = COALESCE(${params.status ?? null}, status) 103 | WHERE id = ${id} 104 | RETURNING * 105 | `; 106 | 107 | return result.length > 0 ? mapChatUserFromDB(result[0]) : null; 108 | }; 109 | 110 | export const updateByUserEmail = async ( 111 | chat_id: string, 112 | email: string, 113 | params: { user_id?: string; status?: ChatUserStatus } 114 | ): Promise => { 115 | const result = await sql` 116 | UPDATE chat_users 117 | SET user_id = COALESCE(${params.user_id ?? null}, user_id), 118 | status = COALESCE(${params.status ?? null}, status) 119 | WHERE chat_id = ${chat_id} AND email = ${email} 120 | RETURNING * 121 | `; 122 | 123 | return result.length > 0 ? mapChatUserFromDB(result[0]) : null; 124 | }; 125 | --------------------------------------------------------------------------------