Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion asynchronous-authorization/langchain-next-js/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ AUTH0_AUDIENCE="https://your.domain.us.langgraph.app"
AUTH0_SCOPE="openid profile email"

# Database configuration
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/ai_documents_db"
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/ai_documents_db"

# LANGGRAPH
LANGGRAPH_API_URL=http://localhost:54367
Expand Down
34 changes: 32 additions & 2 deletions asynchronous-authorization/langchain-next-js/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,33 @@
{
"extends": "next/core-web-vitals"
}
"root": true,
"extends": [
"next/core-web-vitals",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint",
"import"
],
"rules": {
"@typescript-eslint/no-var-requires": "off",
"import/no-commonjs": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-non-null-asserted-optional-chain": "off",
"@typescript-eslint/no-require-imports": "off",
"@typescript-eslint/no-unused-vars": "warn"
},
"overrides": [
{
"files": ["*.config.{js,cjs,mjs}", "scripts/**", "next.config.*", "postcss.config.js", "tailwind.config.*"],
"rules": {
"@typescript-eslint/no-var-requires": "off",
"import/no-commonjs": "off"
}
},
{
"files": ["**/__tests__/**", "**/*.test.{ts,tsx}"],
"env": { "jest": true }
}
]
}
460 changes: 267 additions & 193 deletions asynchronous-authorization/langchain-next-js/bun.lock

Large diffs are not rendered by default.

98 changes: 49 additions & 49 deletions asynchronous-authorization/langchain-next-js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,71 +22,71 @@
"fga:init": "tsx src/lib/fga/fga-init.ts"
},
"engines": {
"node": ">=18"
"node": ">=20 <23"
},
"dependencies": {
"@auth0/ai": "4.0.0",
"@auth0/ai-langchain": "^3.5.0",
"@auth0/nextjs-auth0": "4.4.2",
"@langchain/community": "0.3.53",
"@langchain/core": "^0.3.77",
"@langchain/langgraph": "^0.4.4",
"@langchain/langgraph-sdk": "^0.1.8",
"@langchain/openai": "^0.6.13",
"@radix-ui/react-avatar": "^1.1.7",
"@radix-ui/react-checkbox": "^1.2.3",
"@radix-ui/react-dialog": "^1.1.11",
"@radix-ui/react-dropdown-menu": "^2.1.12",
"@radix-ui/react-popover": "^1.1.11",
"@radix-ui/react-slot": "^1.2.0",
"@types/pg": "^8.15.4",
"@auth0/nextjs-auth0": "^4.10.0",
"@langchain/community": "^0.3.55",
"@langchain/core": "^0.3.75",
"@langchain/langgraph": "^0.4.9",
"@langchain/openai": "^0.6.11",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-slot": "^1.2.3",
"@types/pg": "^8.15.5",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"dotenv": "^17.0.1",
"drizzle-orm": "^0.43.1",
"drizzle-zod": "^0.7.1",
"googleapis": "^148.0.0",
"jose": "^6.1.0",
"langchain": "^0.3.12",
"drizzle-orm": "^0.44.5",
"drizzle-zod": "0.7.1",
"googleapis": "^159.0.0",
"langchain": "^0.3.33",
"langgraph-nextjs-api-passthrough": "^0.1.4",
"lucide-react": "^0.475.0",
"marked": "^15.0.7",
"lucide-react": "^0.543.0",
"marked": "^16.2.1",
"nanoid": "^5.1.5",
"next": "15.2.4",
"next-themes": "^0.4.4",
"nuqs": "^2.4.3",
"next": "^15.5.4",
"next-themes": "^0.4.6",
"nuqs": "^2.6.0",
"pdf-parse": "^1.1.1",
"pg": "^8.16.3",
"postgres": "^3.4.5",
"react": "19.0.0",
"postgres": "^3.4.7",
"react": "^19.1.1",
"react-device-detect": "^2.2.3",
"react-dom": "19.0.0",
"react-markdown": "^10.0.0",
"react-toastify": "11.0.3",
"sonner": "^1.7.2",
"tailwind-merge": "^2.6.0",
"react-dom": "^19.1.1",
"react-markdown": "^10.1.0",
"react-toastify": "^11.0.5",
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
"tailwindcss-animate": "^1.0.7",
"use-stick-to-bottom": "^1.0.44",
"use-stick-to-bottom": "^1.1.1",
"vaul": "^1.1.2",
"zod": "^3.24.2",
"zod": "3.25.76 || ^4",
"zod-to-json-schema": "^3.23.2"
},
"devDependencies": {
"@next/bundle-analyzer": "^15.1.7",
"@types/node": "^22.13.4",
"@next/bundle-analyzer": "^15.5.2",
"@types/node": "^24.3.1",
"@types/pdf-parse": "^1.1.5",
"@types/react": "19.0.9",
"@types/react-dom": "19.0.3",
"autoprefixer": "^10.4.20",
"drizzle-kit": "^0.31.1",
"eslint": "^9.20.1",
"eslint-config-next": "^15.1.7",
"@types/react": "^19.1.12",
"@types/react-dom": "^19.1.9",
"@typescript-eslint/eslint-plugin": "^8.45.0",
"@typescript-eslint/parser": "^8.45.0",
"autoprefixer": "^10.4.21",
"dotenv": "^16.4.5",
"drizzle-kit": "^0.31.4",
"eslint": "^9.36.0",
"eslint-config-next": "^15.5.2",
"eslint-plugin-import": "^2.32.0",
"npm-run-all": "^4.1.5",
"postcss": "8.5.2",
"prettier": "^3.4.2",
"tailwindcss": "3.4.17",
"tsx": "^4.19.4",
"typescript": "5.7.3"
"postcss": "^8.5.6",
"prettier": "^3.6.2",
"tailwindcss": "^3.4.17",
"tsx": "^4.20.5",
"typescript": "^5.9.2"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ module.exports = {
tailwindcss: {},
autoprefixer: {},
},
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,119 @@
import { Message } from '@langchain/langgraph-sdk';
import { Message, AIMessage } from '@langchain/langgraph-sdk';
import { Loader2, CheckCircle } from 'lucide-react';

import { cn } from '@/utils/cn';
import { MemoizedMarkdown } from './memoized-markdown';

export function ChatMessageBubble(props: { message: Message; aiEmoji?: string }) {
return ['human', 'ai'].includes(props.message.type) && props.message.content.length > 0 ? (
function ToolCallDisplay({
toolCall,
isRunning,
messageContent
}: {
toolCall: NonNullable<AIMessage['tool_calls']>[0];
isRunning: boolean;
messageContent?: string;
}) {
return (
<div className="border border-gray-200 rounded-lg p-3 mb-2 bg-gray-50 dark:bg-gray-800 dark:border-gray-600">
<div className="flex items-center gap-2 mb-2">
{isRunning ? (
<Loader2 className="w-4 h-4 animate-spin text-blue-500" />
) : (
<CheckCircle className="w-4 h-4 text-green-500" />
)}
<span className="font-medium text-sm text-gray-900 dark:text-gray-100">
{isRunning ? `Calling ${toolCall.name}...` : `Called ${toolCall.name}`}
</span>
</div>

{/* Show tool arguments/input */}
{toolCall.args && Object.keys(toolCall.args).length > 0 && (
<div className="mb-2">
<div className="text-xs text-gray-600 dark:text-gray-400 mb-1 font-medium">Input:</div>
<div className="bg-white dark:bg-gray-900 rounded px-3 py-2 text-xs font-mono border border-gray-200 dark:border-gray-700">
{JSON.stringify(toolCall.args, null, 2)}
</div>
</div>
)}

{/* Show tool result/output */}
{messageContent && !isRunning && (
<div>
<div className="text-xs text-gray-600 dark:text-gray-400 mb-1 font-medium">Output:</div>
<div className="bg-green-50 dark:bg-green-900/20 rounded px-3 py-2 text-xs border border-green-200 dark:border-green-800">
<span className="text-green-800 dark:text-green-200">
{messageContent}
</span>
</div>
</div>
)}
</div>
);
}

export function ChatMessageBubble(props: { message: Message; aiEmoji?: string; allMessages?: Message[] }) {
const toolCalls = props.message.type === 'ai' ? props.message.tool_calls || [] : [];

// Get message content as string
const getMessageContent = (message: Message): string => {
if (typeof message.content === 'string') {
return message.content;
}
if (Array.isArray(message.content)) {
return message.content
.map(part => {
if (typeof part === 'string') return part;
if (typeof part === 'object' && 'text' in part) return part.text;
return '';
})
.join('');
}
return '';
};

const content = getMessageContent(props.message);
const hasContent = content.length > 0;
const hasToolCalls = toolCalls.length > 0;

// Check if tool calls have corresponding tool result messages
const hasToolResults = hasToolCalls && props.allMessages && toolCalls.some(toolCall =>
props.allMessages!.some(msg =>
msg.type === 'tool' &&
'tool_call_id' in msg &&
msg.tool_call_id === toolCall.id
)
);

// Simple logic: Running = tool calls exist but no tool result messages yet
const isRunning = hasToolCalls && !hasToolResults;

// Get tool result content for display
const getToolResultContent = () => {
if (!hasToolCalls || !props.allMessages) return '';

for (const toolCall of toolCalls) {
const toolResult = props.allMessages.find(msg =>
msg.type === 'tool' &&
'tool_call_id' in msg &&
msg.tool_call_id === toolCall.id
);
if (toolResult) {
return getMessageContent(toolResult);
}
}
return '';
};

const toolResultContent = getToolResultContent();

// Show tool calls if we have any
const shouldShowToolCalls = hasToolCalls;

if (!(['human', 'ai'].includes(props.message.type) && (hasContent || shouldShowToolCalls))) {
return null;
}

return (
<div
className={cn(
`rounded-[24px] max-w-[80%] mb-8 flex`,
Expand All @@ -18,8 +127,22 @@ export function ChatMessageBubble(props: { message: Message; aiEmoji?: string })
</div>
)}
<div className="chat-message-bubble whitespace-pre-wrap flex flex-col prose dark:prose-invert max-w-none">
<MemoizedMarkdown content={props.message.content as string} id={props.message.id ?? ''} />
{shouldShowToolCalls && (
<div className="space-y-2 mb-3 not-prose">
{toolCalls.map((toolCall) => (
<ToolCallDisplay
key={toolCall.id}
toolCall={toolCall}
isRunning={isRunning}
messageContent={toolResultContent}
/>
))}
</div>
)}
{hasContent && (
<MemoizedMarkdown content={content} id={props.message.id ?? ''} />
)}
</div>
</div>
) : null;
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ function ChatMessages(props: {
return (
<div className="flex flex-col max-w-[768px] mx-auto pb-12 w-full">
{props.messages.map((m, i) => {
return <ChatMessageBubble key={m.id} message={m} aiEmoji={props.aiEmoji} />;
return <ChatMessageBubble key={m.id} message={m} aiEmoji={props.aiEmoji} allMessages={props.messages} />;
})}
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@ import { shopOnlineTool } from './tools/shop-online';

const date = new Date().toISOString();

const AGENT_SYSTEM_TEMPLATE = `You are a personal assistant named Assistant0. You are a helpful assistant that can answer questions and help with tasks. You have access to a set of tools, use the tools as needed to answer the user's question. Render the email body as a markdown block, do not wrap it in code blocks. Today is ${date}.`;
const AGENT_SYSTEM_TEMPLATE = `You are a personal assistant named Assistant0. You are a helpful assistant that can answer questions and help with tasks.
You have access to a set of tools. When using tools, you MUST provide valid JSON arguments. Always format tool call arguments as proper JSON objects.
For example, when calling shop_online tool, format like this:
{"product": "iPhone", "qty": 1, "priceLimit": 1000}
Use the tools as needed to answer the user's question. Render the email body as a markdown block, do not wrap it in code blocks. Today is ${date}.`;

const llm = new ChatOpenAI({
model: 'gpt-4o',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { sql } from 'drizzle-orm';
import { text, varchar, timestamp, pgTable, customType } from 'drizzle-orm/pg-core';
import { varchar, timestamp, pgTable, customType } from 'drizzle-orm/pg-core';
import { createSelectSchema } from 'drizzle-zod';
import { z } from 'zod';

Expand All @@ -18,20 +18,17 @@ export const documents = pgTable('documents', {
content: bytea('content').notNull(),
fileName: varchar('file_name', { length: 300 }).notNull(),
fileType: varchar('file_type', { length: 100 }).notNull(),
createdAt: timestamp('created_at')
.notNull()
.default(sql`now()`),
updatedAt: timestamp('updated_at')
.notNull()
.default(sql`now()`),
createdAt: timestamp('created_at').notNull().default(sql`now()`),
updatedAt: timestamp('updated_at').notNull().default(sql`now()`),
userId: varchar('user_id', { length: 191 }).notNull(),
userEmail: varchar('user_email', { length: 191 }).notNull(),
sharedWith: varchar('shared_with', { length: 300 }).array(),
});

export const documentSchema = createSelectSchema(documents).extend({});
export const documentSchema = createSelectSchema(documents, {
content: z.instanceof(Buffer),
}).extend({});

// Schema for documents - used to validate API requests
export const insertDocumentSchema = documentSchema.omit({
id: true,
createdAt: true,
Expand All @@ -40,6 +37,5 @@ export const insertDocumentSchema = documentSchema.omit({
userEmail: true,
});

// Type for documents - used to type API request params and within Components
export type DocumentParams = z.infer<typeof documentSchema>;
export type NewDocumentParams = z.infer<typeof insertDocumentSchema>;
Loading