mirror of
https://github.com/eliasstepanik/core.git
synced 2026-01-11 18:48:27 +00:00
1. Remove chat and deep-search from trigger
2. Add ai/sdk for chat UI 3. Added a better model manager
This commit is contained in:
parent
cf91a824d1
commit
8836849310
@ -1,4 +1,4 @@
|
||||
VERSION=0.1.25
|
||||
VERSION=0.1.26
|
||||
|
||||
# Nest run in docker, change host to database container name
|
||||
DB_HOST=localhost
|
||||
|
||||
@ -72,27 +72,6 @@ export const conversationTitleQueue = new Queue("conversation-title-queue", {
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Deep search queue
|
||||
*/
|
||||
export const deepSearchQueue = new Queue("deep-search-queue", {
|
||||
connection: getRedisConnection(),
|
||||
defaultJobOptions: {
|
||||
attempts: 3,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: 2000,
|
||||
},
|
||||
removeOnComplete: {
|
||||
age: 3600,
|
||||
count: 1000,
|
||||
},
|
||||
removeOnFail: {
|
||||
age: 86400,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Session compaction queue
|
||||
*/
|
||||
|
||||
@ -13,7 +13,6 @@ import {
|
||||
ingestWorker,
|
||||
documentIngestWorker,
|
||||
conversationTitleWorker,
|
||||
deepSearchWorker,
|
||||
sessionCompactionWorker,
|
||||
closeAllWorkers,
|
||||
} from "./workers";
|
||||
@ -21,7 +20,6 @@ import {
|
||||
ingestQueue,
|
||||
documentIngestQueue,
|
||||
conversationTitleQueue,
|
||||
deepSearchQueue,
|
||||
sessionCompactionQueue,
|
||||
} from "./queues";
|
||||
import {
|
||||
@ -47,7 +45,7 @@ export async function initWorkers(): Promise<void> {
|
||||
conversationTitleQueue,
|
||||
"conversation-title",
|
||||
);
|
||||
setupWorkerLogging(deepSearchWorker, deepSearchQueue, "deep-search");
|
||||
|
||||
setupWorkerLogging(
|
||||
sessionCompactionWorker,
|
||||
sessionCompactionQueue,
|
||||
@ -68,7 +66,7 @@ export async function initWorkers(): Promise<void> {
|
||||
queue: conversationTitleQueue,
|
||||
name: "conversation-title",
|
||||
},
|
||||
{ worker: deepSearchWorker, queue: deepSearchQueue, name: "deep-search" },
|
||||
|
||||
{
|
||||
worker: sessionCompactionWorker,
|
||||
queue: sessionCompactionQueue,
|
||||
@ -88,7 +86,7 @@ export async function initWorkers(): Promise<void> {
|
||||
logger.log(
|
||||
`✓ Conversation title worker: ${conversationTitleWorker.name} (concurrency: 10)`,
|
||||
);
|
||||
logger.log(`✓ Deep search worker: ${deepSearchWorker.name} (concurrency: 5)`);
|
||||
|
||||
logger.log(
|
||||
`✓ Session compaction worker: ${sessionCompactionWorker.name} (concurrency: 3)`,
|
||||
);
|
||||
|
||||
@ -18,7 +18,6 @@ async function getAllQueues() {
|
||||
ingestQueue,
|
||||
documentIngestQueue,
|
||||
conversationTitleQueue,
|
||||
deepSearchQueue,
|
||||
sessionCompactionQueue,
|
||||
} = await import("../queues");
|
||||
|
||||
@ -26,7 +25,6 @@ async function getAllQueues() {
|
||||
ingestQueue,
|
||||
documentIngestQueue,
|
||||
conversationTitleQueue,
|
||||
deepSearchQueue,
|
||||
sessionCompactionQueue,
|
||||
];
|
||||
}
|
||||
|
||||
@ -18,10 +18,7 @@ import {
|
||||
processConversationTitleCreation,
|
||||
type CreateConversationTitlePayload,
|
||||
} from "~/jobs/conversation/create-title.logic";
|
||||
import {
|
||||
processDeepSearch,
|
||||
type ProcessDeepSearchPayload,
|
||||
} from "~/jobs/deep-search/deep-search.logic";
|
||||
|
||||
import {
|
||||
processSessionCompaction,
|
||||
type SessionCompactionPayload,
|
||||
@ -58,14 +55,6 @@ export const ingestWorker = new Worker(
|
||||
},
|
||||
);
|
||||
|
||||
ingestWorker.on("completed", (job) => {
|
||||
logger.log(`Job ${job.id} completed`);
|
||||
});
|
||||
|
||||
ingestWorker.on("failed", (job, error) => {
|
||||
logger.error(`Job ${job?.id} failed: ${error}`);
|
||||
});
|
||||
|
||||
/**
|
||||
* Document ingestion worker
|
||||
* Handles document-level ingestion with differential processing
|
||||
@ -89,14 +78,6 @@ export const documentIngestWorker = new Worker(
|
||||
},
|
||||
);
|
||||
|
||||
documentIngestWorker.on("completed", (job) => {
|
||||
logger.log(`Document job ${job.id} completed`);
|
||||
});
|
||||
|
||||
documentIngestWorker.on("failed", (job, error) => {
|
||||
logger.error(`Document job ${job?.id} failed: ${error}`);
|
||||
});
|
||||
|
||||
/**
|
||||
* Conversation title creation worker
|
||||
*/
|
||||
@ -112,37 +93,6 @@ export const conversationTitleWorker = new Worker(
|
||||
},
|
||||
);
|
||||
|
||||
conversationTitleWorker.on("completed", (job) => {
|
||||
logger.log(`Conversation title job ${job.id} completed`);
|
||||
});
|
||||
|
||||
conversationTitleWorker.on("failed", (job, error) => {
|
||||
logger.error(`Conversation title job ${job?.id} failed: ${error}`);
|
||||
});
|
||||
|
||||
/**
|
||||
* Deep search worker (non-streaming version for BullMQ)
|
||||
*/
|
||||
export const deepSearchWorker = new Worker(
|
||||
"deep-search-queue",
|
||||
async (job) => {
|
||||
const payload = job.data as ProcessDeepSearchPayload;
|
||||
return await processDeepSearch(payload);
|
||||
},
|
||||
{
|
||||
connection: getRedisConnection(),
|
||||
concurrency: 5, // Process up to 5 searches in parallel
|
||||
},
|
||||
);
|
||||
|
||||
deepSearchWorker.on("completed", (job) => {
|
||||
logger.log(`Deep search job ${job.id} completed`);
|
||||
});
|
||||
|
||||
deepSearchWorker.on("failed", (job, error) => {
|
||||
logger.error(`Deep search job ${job?.id} failed: ${error}`);
|
||||
});
|
||||
|
||||
/**
|
||||
* Session compaction worker
|
||||
*/
|
||||
@ -158,14 +108,6 @@ export const sessionCompactionWorker = new Worker(
|
||||
},
|
||||
);
|
||||
|
||||
sessionCompactionWorker.on("completed", (job) => {
|
||||
logger.log(`Session compaction job ${job.id} completed`);
|
||||
});
|
||||
|
||||
sessionCompactionWorker.on("failed", (job, error) => {
|
||||
logger.error(`Session compaction job ${job?.id} failed: ${error}`);
|
||||
});
|
||||
|
||||
/**
|
||||
* Graceful shutdown handler
|
||||
*/
|
||||
@ -174,7 +116,7 @@ export async function closeAllWorkers(): Promise<void> {
|
||||
ingestWorker.close(),
|
||||
documentIngestWorker.close(),
|
||||
conversationTitleWorker.close(),
|
||||
deepSearchWorker.close(),
|
||||
|
||||
sessionCompactionWorker.close(),
|
||||
]);
|
||||
logger.log("All BullMQ workers closed");
|
||||
|
||||
@ -1,38 +1,33 @@
|
||||
import { EditorContent, useEditor } from "@tiptap/react";
|
||||
|
||||
import { useEffect, memo } from "react";
|
||||
import { UserTypeEnum } from "@core/types";
|
||||
import { type ConversationHistory } from "@core/database";
|
||||
import { cn } from "~/lib/utils";
|
||||
import { extensionsForConversation } from "./editor-extensions";
|
||||
import { skillExtension } from "../editor/skill-extension";
|
||||
import { type UIMessage } from "ai";
|
||||
|
||||
interface AIConversationItemProps {
|
||||
conversationHistory: ConversationHistory;
|
||||
message: UIMessage;
|
||||
}
|
||||
|
||||
const ConversationItemComponent = ({
|
||||
conversationHistory,
|
||||
}: AIConversationItemProps) => {
|
||||
const isUser =
|
||||
conversationHistory.userType === UserTypeEnum.User ||
|
||||
conversationHistory.userType === UserTypeEnum.System;
|
||||
|
||||
const id = `a${conversationHistory.id.replace(/-/g, "")}`;
|
||||
const ConversationItemComponent = ({ message }: AIConversationItemProps) => {
|
||||
const isUser = message.role === "user" || false;
|
||||
const textPart = message.parts.find((part) => part.type === "text");
|
||||
|
||||
const editor = useEditor({
|
||||
extensions: [...extensionsForConversation, skillExtension],
|
||||
editable: false,
|
||||
content: conversationHistory.message,
|
||||
content: textPart ? textPart.text : "",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
editor?.commands.setContent(conversationHistory.message);
|
||||
|
||||
if (textPart) {
|
||||
editor?.commands.setContent(textPart.text);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [id, conversationHistory.message]);
|
||||
}, [message]);
|
||||
|
||||
if (!conversationHistory.message) {
|
||||
if (!message) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -51,10 +46,10 @@ const ConversationItemComponent = ({
|
||||
};
|
||||
|
||||
// Memoize to prevent unnecessary re-renders
|
||||
export const ConversationItem = memo(ConversationItemComponent, (prevProps, nextProps) => {
|
||||
export const ConversationItem = memo(
|
||||
ConversationItemComponent,
|
||||
(prevProps, nextProps) => {
|
||||
// Only re-render if the conversation history ID or message changed
|
||||
return (
|
||||
prevProps.conversationHistory.id === nextProps.conversationHistory.id &&
|
||||
prevProps.conversationHistory.message === nextProps.conversationHistory.message
|
||||
);
|
||||
});
|
||||
return prevProps.message === nextProps.message;
|
||||
},
|
||||
);
|
||||
|
||||
@ -13,33 +13,26 @@ import { Form, useSubmit, useActionData } from "@remix-run/react";
|
||||
|
||||
interface ConversationTextareaProps {
|
||||
defaultValue?: string;
|
||||
conversationId: string;
|
||||
placeholder?: string;
|
||||
isLoading?: boolean;
|
||||
className?: string;
|
||||
onChange?: (text: string) => void;
|
||||
disabled?: boolean;
|
||||
onConversationCreated?: (conversation: any) => void;
|
||||
onConversationCreated?: (message: string) => void;
|
||||
stop?: () => void;
|
||||
}
|
||||
|
||||
export function ConversationTextarea({
|
||||
defaultValue,
|
||||
isLoading = false,
|
||||
placeholder,
|
||||
conversationId,
|
||||
onChange,
|
||||
onConversationCreated,
|
||||
stop,
|
||||
}: ConversationTextareaProps) {
|
||||
const [text, setText] = useState(defaultValue ?? "");
|
||||
const [editor, setEditor] = useState<Editor>();
|
||||
const submit = useSubmit();
|
||||
const actionData = useActionData<{ conversation?: any }>();
|
||||
|
||||
useEffect(() => {
|
||||
if (actionData?.conversation && onConversationCreated) {
|
||||
onConversationCreated(actionData.conversation);
|
||||
}
|
||||
}, [actionData]);
|
||||
|
||||
const onUpdate = (editor: Editor) => {
|
||||
setText(editor.getHTML());
|
||||
@ -51,53 +44,13 @@ export function ConversationTextarea({
|
||||
return;
|
||||
}
|
||||
|
||||
const data = isLoading ? {} : { message: text, conversationId };
|
||||
|
||||
// When conversationId exists and not stopping, submit to current route
|
||||
// When isLoading (stopping), submit to the specific conversation route
|
||||
submit(data as any, {
|
||||
action: isLoading
|
||||
? `/home/conversation/${conversationId}`
|
||||
: conversationId
|
||||
? `/home/conversation/${conversationId}`
|
||||
: "/home/conversation",
|
||||
method: "post",
|
||||
});
|
||||
onConversationCreated && onConversationCreated(text);
|
||||
|
||||
editor?.commands.clearContent(true);
|
||||
setText("");
|
||||
}, [editor, text]);
|
||||
|
||||
// Send message to API
|
||||
const submitForm = useCallback(
|
||||
async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const data = isLoading
|
||||
? {}
|
||||
: { message: text, title: text, conversationId };
|
||||
|
||||
submit(data as any, {
|
||||
action: isLoading
|
||||
? `/home/conversation/${conversationId}`
|
||||
: conversationId
|
||||
? `/home/conversation/${conversationId}`
|
||||
: "/home/conversation",
|
||||
method: "post",
|
||||
});
|
||||
|
||||
editor?.commands.clearContent(true);
|
||||
setText("");
|
||||
e.preventDefault();
|
||||
},
|
||||
[text, conversationId],
|
||||
);
|
||||
|
||||
return (
|
||||
<Form
|
||||
action="/home/conversation"
|
||||
method="post"
|
||||
onSubmit={(e) => submitForm(e)}
|
||||
className="pt-2"
|
||||
>
|
||||
<div className="bg-background-3 rounded-lg border-1 border-gray-300 py-2">
|
||||
<EditorRoot>
|
||||
<EditorContent
|
||||
@ -165,7 +118,13 @@ export function ConversationTextarea({
|
||||
<Button
|
||||
variant="default"
|
||||
className="gap-1 shadow-none transition-all duration-500 ease-in-out"
|
||||
type="submit"
|
||||
onClick={() => {
|
||||
if (!isLoading) {
|
||||
handleSend();
|
||||
} else {
|
||||
stop && stop();
|
||||
}
|
||||
}}
|
||||
size="lg"
|
||||
>
|
||||
{isLoading ? (
|
||||
@ -179,6 +138,5 @@ export function ConversationTextarea({
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
@ -47,8 +47,16 @@ const EnvironmentSchema = z
|
||||
POSTHOG_PROJECT_KEY: z
|
||||
.string()
|
||||
.default("phc_SwfGIzzX5gh5bazVWoRxZTBhkr7FwvzArS0NRyGXm1a"),
|
||||
TELEMETRY_ENABLED: z.coerce.boolean().default(true),
|
||||
TELEMETRY_ANONYMOUS: z.coerce.boolean().default(false),
|
||||
TELEMETRY_ENABLED: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("true")
|
||||
.transform((val) => val !== "false" && val !== "0"),
|
||||
TELEMETRY_ANONYMOUS: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("false")
|
||||
.transform((val) => val === "true" || val === "1"),
|
||||
|
||||
//storage
|
||||
ACCESS_KEY_ID: z.string().optional(),
|
||||
@ -59,12 +67,20 @@ const EnvironmentSchema = z
|
||||
AUTH_GOOGLE_CLIENT_ID: z.string().optional(),
|
||||
AUTH_GOOGLE_CLIENT_SECRET: z.string().optional(),
|
||||
|
||||
ENABLE_EMAIL_LOGIN: z.coerce.boolean().default(true),
|
||||
ENABLE_EMAIL_LOGIN: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("true")
|
||||
.transform((val) => val !== "false" && val !== "0"),
|
||||
|
||||
//Redis
|
||||
REDIS_HOST: z.string().default("localhost"),
|
||||
REDIS_PORT: z.coerce.number().default(6379),
|
||||
REDIS_TLS_DISABLED: z.coerce.boolean().default(true),
|
||||
REDIS_TLS_DISABLED: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("true")
|
||||
.transform((val) => val !== "false" && val !== "0"),
|
||||
|
||||
//Neo4j
|
||||
NEO4J_URI: z.string(),
|
||||
@ -72,8 +88,9 @@ const EnvironmentSchema = z
|
||||
NEO4J_PASSWORD: z.string(),
|
||||
|
||||
//OpenAI
|
||||
OPENAI_API_KEY: z.string(),
|
||||
OPENAI_API_KEY: z.string().optional(),
|
||||
ANTHROPIC_API_KEY: z.string().optional(),
|
||||
GOOGLE_GENERATIVE_AI_API_KEY: z.string().optional(),
|
||||
|
||||
EMAIL_TRANSPORT: z.string().optional(),
|
||||
FROM_EMAIL: z.string().optional(),
|
||||
@ -81,7 +98,10 @@ const EnvironmentSchema = z
|
||||
RESEND_API_KEY: z.string().optional(),
|
||||
SMTP_HOST: z.string().optional(),
|
||||
SMTP_PORT: z.coerce.number().optional(),
|
||||
SMTP_SECURE: z.coerce.boolean().optional(),
|
||||
SMTP_SECURE: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((val) => val === "true" || val === "1"),
|
||||
SMTP_USER: z.string().optional(),
|
||||
SMTP_PASSWORD: z.string().optional(),
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { LLMMappings } from "@core/types";
|
||||
import { generate } from "~/trigger/chat/stream-utils";
|
||||
import { conversationTitlePrompt } from "~/trigger/conversation/prompt";
|
||||
import { prisma } from "~/trigger/utils/prisma";
|
||||
import { logger } from "~/services/logger.service";
|
||||
import { generateText, type LanguageModel } from "ai";
|
||||
import { getModel } from "~/lib/model.server";
|
||||
|
||||
export interface CreateConversationTitlePayload {
|
||||
conversationId: string;
|
||||
@ -24,8 +24,9 @@ export async function processConversationTitleCreation(
|
||||
): Promise<CreateConversationTitleResult> {
|
||||
try {
|
||||
let conversationTitleResponse = "";
|
||||
const gen = generate(
|
||||
[
|
||||
const { text } = await generateText({
|
||||
model: getModel() as LanguageModel,
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: conversationTitlePrompt.replace(
|
||||
@ -34,24 +35,9 @@ export async function processConversationTitleCreation(
|
||||
),
|
||||
},
|
||||
],
|
||||
false,
|
||||
() => {},
|
||||
undefined,
|
||||
"",
|
||||
LLMMappings.GPT41,
|
||||
);
|
||||
});
|
||||
|
||||
for await (const chunk of gen) {
|
||||
if (typeof chunk === "string") {
|
||||
conversationTitleResponse += chunk;
|
||||
} else if (chunk && typeof chunk === "object" && chunk.message) {
|
||||
conversationTitleResponse += chunk.message;
|
||||
}
|
||||
}
|
||||
|
||||
const outputMatch = conversationTitleResponse.match(
|
||||
/<output>(.*?)<\/output>/s,
|
||||
);
|
||||
const outputMatch = text.match(/<output>(.*?)<\/output>/s);
|
||||
|
||||
logger.info(`Conversation title data: ${JSON.stringify(outputMatch)}`);
|
||||
|
||||
|
||||
@ -1,104 +0,0 @@
|
||||
import { type CoreMessage } from "ai";
|
||||
import { logger } from "~/services/logger.service";
|
||||
import { nanoid } from "nanoid";
|
||||
import {
|
||||
deletePersonalAccessToken,
|
||||
getOrCreatePersonalAccessToken,
|
||||
} from "~/trigger/utils/utils";
|
||||
import { getReActPrompt } from "~/trigger/deep-search/prompt";
|
||||
import { createSearchMemoryTool } from "~/trigger/deep-search/utils";
|
||||
import { run } from "~/trigger/deep-search/deep-search-utils";
|
||||
import { AgentMessageType } from "~/trigger/chat/types";
|
||||
|
||||
export interface ProcessDeepSearchPayload {
|
||||
content: string;
|
||||
userId: string;
|
||||
metadata?: any;
|
||||
intentOverride?: string;
|
||||
}
|
||||
|
||||
export interface ProcessDeepSearchResult {
|
||||
success: boolean;
|
||||
synthesis?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Core business logic for deep search (non-streaming version for BullMQ)
|
||||
* This is shared logic, but the streaming happens in Trigger.dev via metadata.stream
|
||||
*/
|
||||
export async function processDeepSearch(
|
||||
payload: ProcessDeepSearchPayload,
|
||||
): Promise<ProcessDeepSearchResult> {
|
||||
const { content, userId, metadata: meta, intentOverride } = payload;
|
||||
|
||||
const randomKeyName = `deepSearch_${nanoid(10)}`;
|
||||
|
||||
// Get or create token for search API calls
|
||||
const pat = await getOrCreatePersonalAccessToken({
|
||||
name: randomKeyName,
|
||||
userId: userId as string,
|
||||
});
|
||||
|
||||
if (!pat?.token) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Failed to create personal access token",
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// Create search tool that agent will use
|
||||
const searchTool = createSearchMemoryTool(pat.token);
|
||||
|
||||
// Build initial messages with ReAct prompt
|
||||
const initialMessages: CoreMessage[] = [
|
||||
{
|
||||
role: "system",
|
||||
content: getReActPrompt(meta, intentOverride),
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: `CONTENT TO ANALYZE:\n${content}\n\nPlease search my memory for relevant context and synthesize what you find.`,
|
||||
},
|
||||
];
|
||||
|
||||
// Run the ReAct loop generator
|
||||
const llmResponse = run(initialMessages, searchTool);
|
||||
|
||||
let synthesis = "";
|
||||
|
||||
// For BullMQ: iterate without streaming, just accumulate the final synthesis
|
||||
for await (const step of llmResponse) {
|
||||
// MESSAGE_CHUNK: Final synthesis - accumulate
|
||||
if (step.type === AgentMessageType.MESSAGE_CHUNK) {
|
||||
synthesis += step.message;
|
||||
}
|
||||
|
||||
// STREAM_END: Loop completed
|
||||
if (step.type === AgentMessageType.STREAM_END) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await deletePersonalAccessToken(pat?.id);
|
||||
|
||||
// Clean up any remaining tags
|
||||
synthesis = synthesis
|
||||
.replace(/<final_response>/gi, "")
|
||||
.replace(/<\/final_response>/gi, "")
|
||||
.trim();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
synthesis,
|
||||
};
|
||||
} catch (error: any) {
|
||||
await deletePersonalAccessToken(pat?.id);
|
||||
logger.error(`Deep search error: ${error}`);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,31 +1,23 @@
|
||||
import {
|
||||
type CoreMessage,
|
||||
type LanguageModelV1,
|
||||
embed,
|
||||
generateText,
|
||||
streamText,
|
||||
} from "ai";
|
||||
import { type CoreMessage, embed, generateText, streamText } from "ai";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
import { logger } from "~/services/logger.service";
|
||||
|
||||
import { createOllama, type OllamaProvider } from "ollama-ai-provider";
|
||||
import { createOllama } from "ollama-ai-provider-v2";
|
||||
import { anthropic } from "@ai-sdk/anthropic";
|
||||
import { google } from "@ai-sdk/google";
|
||||
import { createAmazonBedrock } from "@ai-sdk/amazon-bedrock";
|
||||
import { fromNodeProviderChain } from "@aws-sdk/credential-providers";
|
||||
|
||||
export type ModelComplexity = 'high' | 'low';
|
||||
export type ModelComplexity = "high" | "low";
|
||||
|
||||
/**
|
||||
* Get the appropriate model for a given complexity level.
|
||||
* HIGH complexity uses the configured MODEL.
|
||||
* LOW complexity automatically downgrades to cheaper variants if possible.
|
||||
*/
|
||||
export function getModelForTask(complexity: ModelComplexity = 'high'): string {
|
||||
const baseModel = process.env.MODEL || 'gpt-4.1-2025-04-14';
|
||||
export function getModelForTask(complexity: ModelComplexity = "high"): string {
|
||||
const baseModel = process.env.MODEL || "gpt-4.1-2025-04-14";
|
||||
|
||||
// HIGH complexity - always use the configured model
|
||||
if (complexity === 'high') {
|
||||
if (complexity === "high") {
|
||||
return baseModel;
|
||||
}
|
||||
|
||||
@ -33,29 +25,73 @@ export function getModelForTask(complexity: ModelComplexity = 'high'): string {
|
||||
// If already using a cheap model, keep it
|
||||
const downgrades: Record<string, string> = {
|
||||
// OpenAI downgrades
|
||||
'gpt-5-2025-08-07': 'gpt-5-mini-2025-08-07',
|
||||
'gpt-4.1-2025-04-14': 'gpt-4.1-mini-2025-04-14',
|
||||
"gpt-5-2025-08-07": "gpt-5-mini-2025-08-07",
|
||||
"gpt-4.1-2025-04-14": "gpt-4.1-mini-2025-04-14",
|
||||
|
||||
// Anthropic downgrades
|
||||
'claude-sonnet-4-5': 'claude-3-5-haiku-20241022',
|
||||
'claude-3-7-sonnet-20250219': 'claude-3-5-haiku-20241022',
|
||||
'claude-3-opus-20240229': 'claude-3-5-haiku-20241022',
|
||||
"claude-sonnet-4-5": "claude-3-5-haiku-20241022",
|
||||
"claude-3-7-sonnet-20250219": "claude-3-5-haiku-20241022",
|
||||
"claude-3-opus-20240229": "claude-3-5-haiku-20241022",
|
||||
|
||||
// Google downgrades
|
||||
'gemini-2.5-pro-preview-03-25': 'gemini-2.5-flash-preview-04-17',
|
||||
'gemini-2.0-flash': 'gemini-2.0-flash-lite',
|
||||
"gemini-2.5-pro-preview-03-25": "gemini-2.5-flash-preview-04-17",
|
||||
"gemini-2.0-flash": "gemini-2.0-flash-lite",
|
||||
|
||||
// AWS Bedrock downgrades (keep same model - already cost-optimized)
|
||||
'us.amazon.nova-premier-v1:0': 'us.amazon.nova-premier-v1:0',
|
||||
"us.amazon.nova-premier-v1:0": "us.amazon.nova-premier-v1:0",
|
||||
};
|
||||
|
||||
return downgrades[baseModel] || baseModel;
|
||||
}
|
||||
|
||||
export const getModel = (takeModel?: string) => {
|
||||
let model = takeModel;
|
||||
|
||||
const anthropicKey = process.env.ANTHROPIC_API_KEY;
|
||||
const googleKey = process.env.GOOGLE_GENERATIVE_AI_API_KEY;
|
||||
const openaiKey = process.env.OPENAI_API_KEY;
|
||||
let ollamaUrl = process.env.OLLAMA_URL;
|
||||
model = model || process.env.MODEL || "gpt-4.1-2025-04-14";
|
||||
|
||||
let modelInstance;
|
||||
let modelTemperature = Number(process.env.MODEL_TEMPERATURE) || 1;
|
||||
ollamaUrl = undefined;
|
||||
|
||||
// First check if Ollama URL exists and use Ollama
|
||||
if (ollamaUrl) {
|
||||
const ollama = createOllama({
|
||||
baseURL: ollamaUrl,
|
||||
});
|
||||
modelInstance = ollama(model || "llama2"); // Default to llama2 if no model specified
|
||||
} else {
|
||||
// If no Ollama, check other models
|
||||
|
||||
if (model.includes("claude")) {
|
||||
if (!anthropicKey) {
|
||||
throw new Error("No Anthropic API key found. Set ANTHROPIC_API_KEY");
|
||||
}
|
||||
modelInstance = anthropic(model);
|
||||
modelTemperature = 0.5;
|
||||
} else if (model.includes("gemini")) {
|
||||
if (!googleKey) {
|
||||
throw new Error("No Google API key found. Set GOOGLE_API_KEY");
|
||||
}
|
||||
modelInstance = google(model);
|
||||
} else {
|
||||
if (!openaiKey) {
|
||||
throw new Error("No OpenAI API key found. Set OPENAI_API_KEY");
|
||||
}
|
||||
modelInstance = openai(model);
|
||||
}
|
||||
|
||||
return modelInstance;
|
||||
}
|
||||
};
|
||||
|
||||
export interface TokenUsage {
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
promptTokens?: number;
|
||||
completionTokens?: number;
|
||||
totalTokens?: number;
|
||||
}
|
||||
|
||||
export async function makeModelCall(
|
||||
@ -63,69 +99,13 @@ export async function makeModelCall(
|
||||
messages: CoreMessage[],
|
||||
onFinish: (text: string, model: string, usage?: TokenUsage) => void,
|
||||
options?: any,
|
||||
complexity: ModelComplexity = 'high',
|
||||
complexity: ModelComplexity = "high",
|
||||
) {
|
||||
let modelInstance: LanguageModelV1 | undefined;
|
||||
let model = getModelForTask(complexity);
|
||||
const ollamaUrl = process.env.OLLAMA_URL;
|
||||
let ollama: OllamaProvider | undefined;
|
||||
logger.info(`complexity: ${complexity}, model: ${model}`);
|
||||
|
||||
if (ollamaUrl) {
|
||||
ollama = createOllama({
|
||||
baseURL: ollamaUrl,
|
||||
});
|
||||
}
|
||||
|
||||
const bedrock = createAmazonBedrock({
|
||||
region: process.env.AWS_REGION || 'us-east-1',
|
||||
credentialProvider: fromNodeProviderChain(),
|
||||
});
|
||||
|
||||
const generateTextOptions: any = {}
|
||||
|
||||
logger.info(
|
||||
`complexity: ${complexity}, model: ${model}`,
|
||||
);
|
||||
switch (model) {
|
||||
case "gpt-4.1-2025-04-14":
|
||||
case "gpt-4.1-mini-2025-04-14":
|
||||
case "gpt-5-mini-2025-08-07":
|
||||
case "gpt-5-2025-08-07":
|
||||
case "gpt-4.1-nano-2025-04-14":
|
||||
modelInstance = openai(model, { ...options });
|
||||
generateTextOptions.temperature = 1
|
||||
break;
|
||||
|
||||
case "claude-3-7-sonnet-20250219":
|
||||
case "claude-3-opus-20240229":
|
||||
case "claude-3-5-haiku-20241022":
|
||||
modelInstance = anthropic(model, { ...options });
|
||||
break;
|
||||
|
||||
case "gemini-2.5-flash-preview-04-17":
|
||||
case "gemini-2.5-pro-preview-03-25":
|
||||
case "gemini-2.0-flash":
|
||||
case "gemini-2.0-flash-lite":
|
||||
modelInstance = google(model, { ...options });
|
||||
break;
|
||||
|
||||
case "us.meta.llama3-3-70b-instruct-v1:0":
|
||||
case "us.deepseek.r1-v1:0":
|
||||
case "qwen.qwen3-32b-v1:0":
|
||||
case "openai.gpt-oss-120b-1:0":
|
||||
case "us.mistral.pixtral-large-2502-v1:0":
|
||||
case "us.amazon.nova-premier-v1:0":
|
||||
modelInstance = bedrock(`${model}`);
|
||||
generateTextOptions.maxTokens = 100000
|
||||
break;
|
||||
|
||||
default:
|
||||
if (ollama) {
|
||||
modelInstance = ollama(model);
|
||||
}
|
||||
logger.warn(`Unsupported model type: ${model}`);
|
||||
break;
|
||||
}
|
||||
const modelInstance = getModel(model);
|
||||
const generateTextOptions: any = {};
|
||||
|
||||
if (!modelInstance) {
|
||||
throw new Error(`Unsupported model type: ${model}`);
|
||||
@ -135,16 +115,21 @@ export async function makeModelCall(
|
||||
return streamText({
|
||||
model: modelInstance,
|
||||
messages,
|
||||
...options,
|
||||
...generateTextOptions,
|
||||
onFinish: async ({ text, usage }) => {
|
||||
const tokenUsage = usage ? {
|
||||
promptTokens: usage.promptTokens,
|
||||
completionTokens: usage.completionTokens,
|
||||
const tokenUsage = usage
|
||||
? {
|
||||
promptTokens: usage.inputTokens,
|
||||
completionTokens: usage.outputTokens,
|
||||
totalTokens: usage.totalTokens,
|
||||
} : undefined;
|
||||
}
|
||||
: undefined;
|
||||
|
||||
if (tokenUsage) {
|
||||
logger.log(`[${complexity.toUpperCase()}] ${model} - Tokens: ${tokenUsage.totalTokens} (prompt: ${tokenUsage.promptTokens}, completion: ${tokenUsage.completionTokens})`);
|
||||
logger.log(
|
||||
`[${complexity.toUpperCase()}] ${model} - Tokens: ${tokenUsage.totalTokens} (prompt: ${tokenUsage.promptTokens}, completion: ${tokenUsage.completionTokens})`,
|
||||
);
|
||||
}
|
||||
|
||||
onFinish(text, model, tokenUsage);
|
||||
@ -158,14 +143,18 @@ export async function makeModelCall(
|
||||
...generateTextOptions,
|
||||
});
|
||||
|
||||
const tokenUsage = usage ? {
|
||||
promptTokens: usage.promptTokens,
|
||||
completionTokens: usage.completionTokens,
|
||||
const tokenUsage = usage
|
||||
? {
|
||||
promptTokens: usage.inputTokens,
|
||||
completionTokens: usage.outputTokens,
|
||||
totalTokens: usage.totalTokens,
|
||||
} : undefined;
|
||||
}
|
||||
: undefined;
|
||||
|
||||
if (tokenUsage) {
|
||||
logger.log(`[${complexity.toUpperCase()}] ${model} - Tokens: ${tokenUsage.totalTokens} (prompt: ${tokenUsage.promptTokens}, completion: ${tokenUsage.completionTokens})`);
|
||||
logger.log(
|
||||
`[${complexity.toUpperCase()}] ${model} - Tokens: ${tokenUsage.totalTokens} (prompt: ${tokenUsage.promptTokens}, completion: ${tokenUsage.completionTokens})`,
|
||||
);
|
||||
}
|
||||
|
||||
onFinish(text, model, tokenUsage);
|
||||
@ -177,7 +166,10 @@ export async function makeModelCall(
|
||||
* Determines if a given model is proprietary (OpenAI, Anthropic, Google, Grok)
|
||||
* or open source (accessed via Bedrock, Ollama, etc.)
|
||||
*/
|
||||
export function isProprietaryModel(modelName?: string, complexity: ModelComplexity = 'high'): boolean {
|
||||
export function isProprietaryModel(
|
||||
modelName?: string,
|
||||
complexity: ModelComplexity = "high",
|
||||
): boolean {
|
||||
const model = modelName || getModelForTask(complexity);
|
||||
if (!model) return false;
|
||||
|
||||
@ -189,7 +181,7 @@ export function isProprietaryModel(modelName?: string, complexity: ModelComplexi
|
||||
/^grok-/, // xAI models
|
||||
];
|
||||
|
||||
return proprietaryPatterns.some(pattern => pattern.test(model));
|
||||
return proprietaryPatterns.some((pattern) => pattern.test(model));
|
||||
}
|
||||
|
||||
export async function getEmbedding(text: string) {
|
||||
|
||||
326
apps/webapp/app/lib/prompt.server.ts
Normal file
326
apps/webapp/app/lib/prompt.server.ts
Normal file
@ -0,0 +1,326 @@
|
||||
import { tool } from "ai";
|
||||
import z from "zod";
|
||||
|
||||
export const REACT_SYSTEM_PROMPT = `
|
||||
You are a helpful AI assistant with access to user memory. Your primary capabilities are:
|
||||
|
||||
1. **Memory-First Approach**: Always check user memory first to understand context and previous interactions
|
||||
2. **Intelligent Information Gathering**: Analyze queries to determine if current information is needed
|
||||
3. **Memory Management**: Help users store, retrieve, and organize information in their memory
|
||||
4. **Contextual Assistance**: Use memory to provide personalized and contextual responses
|
||||
|
||||
<information_gathering>
|
||||
Follow this intelligent approach for information gathering:
|
||||
|
||||
1. **MEMORY FIRST** (Always Required)
|
||||
- Always check memory FIRST using core--search_memory before any other actions
|
||||
- Consider this your highest priority for EVERY interaction - as essential as breathing
|
||||
- Memory provides context, personal preferences, and historical information
|
||||
- Use memory to understand user's background, ongoing projects, and past conversations
|
||||
|
||||
2. **INFORMATION SYNTHESIS** (Combine Sources)
|
||||
- Use memory to personalize current information based on user preferences
|
||||
- Always store new useful information in memory using core--add_memory
|
||||
|
||||
3. **TRAINING KNOWLEDGE** (Foundation)
|
||||
- Use your training knowledge as the foundation for analysis and explanation
|
||||
- Apply training knowledge to interpret and contextualize information from memory
|
||||
- Indicate when you're using training knowledge vs. live information sources
|
||||
|
||||
EXECUTION APPROACH:
|
||||
- Memory search is mandatory for every interaction
|
||||
- Always indicate your information sources in responses
|
||||
</information_gathering>
|
||||
|
||||
<memory>
|
||||
QUERY FORMATION:
|
||||
- Write specific factual statements as queries (e.g., "user email address" not "what is the user's email?")
|
||||
- Create multiple targeted memory queries for complex requests
|
||||
|
||||
KEY QUERY AREAS:
|
||||
- Personal context: user name, location, identity, work context
|
||||
- Project context: repositories, codebases, current work, team members
|
||||
- Task context: recent tasks, ongoing projects, deadlines, priorities
|
||||
- Integration context: GitHub repos, Slack channels, Linear projects, connected services
|
||||
- Communication patterns: email preferences, notification settings, workflow automation
|
||||
- Technical context: coding languages, frameworks, development environment
|
||||
- Collaboration context: team members, project stakeholders, meeting patterns
|
||||
- Preferences: likes, dislikes, communication style, tool preferences
|
||||
- History: previous discussions, past requests, completed work, recurring issues
|
||||
- Automation rules: user-defined workflows, triggers, automation preferences
|
||||
|
||||
MEMORY USAGE:
|
||||
- Execute multiple memory queries in parallel rather than sequentially
|
||||
- Batch related memory queries when possible
|
||||
- Prioritize recent information over older memories
|
||||
- Create comprehensive context-aware queries based on user message/activity content
|
||||
- Extract and query SEMANTIC CONTENT, not just structural metadata
|
||||
- Parse titles, descriptions, and content for actual subject matter keywords
|
||||
- Search internal SOL tasks/conversations that may relate to the same topics
|
||||
- Query ALL relatable concepts, not just direct keywords or IDs
|
||||
- Search for similar past situations, patterns, and related work
|
||||
- Include synonyms, related terms, and contextual concepts in queries
|
||||
- Query user's historical approach to similar requests or activities
|
||||
- Search for connected projects, tasks, conversations, and collaborations
|
||||
- Retrieve workflow patterns and past decision-making context
|
||||
- Query broader domain context beyond immediate request scope
|
||||
- Remember: SOL tracks work that external tools don't - search internal content thoroughly
|
||||
- Blend memory insights naturally into responses
|
||||
- Verify you've checked relevant memory before finalizing ANY response
|
||||
|
||||
</memory>
|
||||
|
||||
<external_services>
|
||||
- To use: load_mcp with EXACT integration name from the available list
|
||||
- Can load multiple at once with an array
|
||||
- Only load when tools are NOT already available in your current toolset
|
||||
- If a tool is already available, use it directly without load_mcp
|
||||
- If requested integration unavailable: inform user politely
|
||||
</external_services>
|
||||
|
||||
<tool_calling>
|
||||
You have tools at your disposal to assist users:
|
||||
|
||||
CORE PRINCIPLES:
|
||||
- Use tools only when necessary for the task at hand
|
||||
- Always check memory FIRST before making other tool calls
|
||||
- Execute multiple operations in parallel whenever possible
|
||||
- Use sequential calls only when output of one is required for input of another
|
||||
|
||||
PARAMETER HANDLING:
|
||||
- Follow tool schemas exactly with all required parameters
|
||||
- Only use values that are:
|
||||
• Explicitly provided by the user (use EXACTLY as given)
|
||||
• Reasonably inferred from context
|
||||
• Retrieved from memory or prior tool calls
|
||||
- Never make up values for required parameters
|
||||
- Omit optional parameters unless clearly needed
|
||||
- Analyze user's descriptive terms for parameter clues
|
||||
|
||||
TOOL SELECTION:
|
||||
- Never call tools not provided in this conversation
|
||||
- Skip tool calls for general questions you can answer directly from memory/knowledge
|
||||
- For identical operations on multiple items, use parallel tool calls
|
||||
- Default to parallel execution (3-5× faster than sequential calls)
|
||||
- You can always access external service tools by loading them with load_mcp first
|
||||
|
||||
TOOL MENTION HANDLING:
|
||||
When user message contains <mention data-id="tool_name" data-label="tool"></mention>:
|
||||
- Extract tool_name from data-id attribute
|
||||
- First check if it's a built-in tool; if not, check EXTERNAL SERVICES TOOLS
|
||||
- If available: Load it with load_mcp and focus on addressing the request with this tool
|
||||
- If unavailable: Inform user and suggest alternatives if possible
|
||||
- For multiple tool mentions: Load all applicable tools in a single load_mcp call
|
||||
|
||||
ERROR HANDLING:
|
||||
- If a tool returns an error, try fixing parameters before retrying
|
||||
- If you can't resolve an error, explain the issue to the user
|
||||
- Consider alternative tools when primary tools are unavailable
|
||||
</tool_calling>
|
||||
|
||||
<communication>
|
||||
Use EXACTLY ONE of these formats for all user-facing communication:
|
||||
|
||||
PROGRESS UPDATES - During processing:
|
||||
- Use the core--progress_update tool to keep users informed
|
||||
- Update users about what you're discovering or doing next
|
||||
- Keep messages clear and user-friendly
|
||||
- Avoid technical jargon
|
||||
|
||||
QUESTIONS - When you need information:
|
||||
<div>
|
||||
<p>[Your question with HTML formatting]</p>
|
||||
</div>
|
||||
|
||||
- Ask questions only when you cannot find information through memory, or tools
|
||||
- Be specific about what you need to know
|
||||
- Provide context for why you're asking
|
||||
|
||||
FINAL ANSWERS - When completing tasks:
|
||||
<div>
|
||||
<p>[Your answer with HTML formatting]</p>
|
||||
</div>
|
||||
|
||||
CRITICAL:
|
||||
- Use ONE format per turn
|
||||
- Apply proper HTML formatting (<h1>, <h2>, <p>, <ul>, <li>, etc.)
|
||||
- Never mix communication formats
|
||||
- Keep responses clear and helpful
|
||||
- Always indicate your information sources (memory, and/or knowledge)
|
||||
</communication>
|
||||
`;
|
||||
|
||||
export const fixedTools = {
|
||||
progressUpdate: tool({
|
||||
description:
|
||||
"Send a progress update to the user about what has been discovered or will be done next in a crisp and user friendly way no technical terms",
|
||||
inputSchema: z.object({
|
||||
message: z.string(),
|
||||
}),
|
||||
execute: async ({ message }: { message: string }) => ({
|
||||
message,
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
export function getReActPrompt(
|
||||
metadata?: { source?: string; url?: string; pageTitle?: string },
|
||||
intentOverride?: string,
|
||||
): string {
|
||||
const contextHints = [];
|
||||
|
||||
if (
|
||||
metadata?.source === "chrome" &&
|
||||
metadata?.url?.includes("mail.google.com")
|
||||
) {
|
||||
contextHints.push("Content is from email - likely reading intent");
|
||||
}
|
||||
if (
|
||||
metadata?.source === "chrome" &&
|
||||
metadata?.url?.includes("calendar.google.com")
|
||||
) {
|
||||
contextHints.push("Content is from calendar - likely meeting prep intent");
|
||||
}
|
||||
if (
|
||||
metadata?.source === "chrome" &&
|
||||
metadata?.url?.includes("docs.google.com")
|
||||
) {
|
||||
contextHints.push(
|
||||
"Content is from document editor - likely writing intent",
|
||||
);
|
||||
}
|
||||
if (metadata?.source === "obsidian") {
|
||||
contextHints.push(
|
||||
"Content is from note editor - likely writing or research intent",
|
||||
);
|
||||
}
|
||||
|
||||
return `You are a memory research agent analyzing content to find relevant context.
|
||||
|
||||
YOUR PROCESS (ReAct Framework):
|
||||
|
||||
1. DECOMPOSE: First, break down the content into structured categories
|
||||
|
||||
Analyze the content and extract:
|
||||
a) ENTITIES: Specific people, project names, tools, products mentioned
|
||||
Example: "John Smith", "Phoenix API", "Redis", "mobile app"
|
||||
|
||||
b) TOPICS & CONCEPTS: Key subjects, themes, domains
|
||||
Example: "authentication", "database design", "performance optimization"
|
||||
|
||||
c) TEMPORAL MARKERS: Time references, deadlines, events
|
||||
Example: "last week's meeting", "Q2 launch", "yesterday's discussion"
|
||||
|
||||
d) ACTIONS & TASKS: What's being done, decided, or requested
|
||||
Example: "implement feature", "review code", "make decision on"
|
||||
|
||||
e) USER INTENT: What is the user trying to accomplish?
|
||||
${intentOverride ? `User specified: "${intentOverride}"` : "Infer from context: reading/writing/meeting prep/research/task tracking/review"}
|
||||
|
||||
2. FORM QUERIES: Create targeted search queries from your decomposition
|
||||
|
||||
Based on decomposition, form specific queries:
|
||||
- Search for each entity by name (people, projects, tools)
|
||||
- Search for topics the user has discussed before
|
||||
- Search for related work or conversations in this domain
|
||||
- Use the user's actual terminology, not generic concepts
|
||||
|
||||
EXAMPLE - Content: "Email from Sarah about the API redesign we discussed last week"
|
||||
Decomposition:
|
||||
- Entities: "Sarah", "API redesign"
|
||||
- Topics: "API design", "redesign"
|
||||
- Temporal: "last week"
|
||||
- Actions: "discussed", "email communication"
|
||||
- Intent: Reading (email) / meeting prep
|
||||
|
||||
Queries to form:
|
||||
✅ "Sarah" (find past conversations with Sarah)
|
||||
✅ "API redesign" or "API design" (find project discussions)
|
||||
✅ "last week" + "Sarah" (find recent context)
|
||||
✅ "meetings" or "discussions" (find related conversations)
|
||||
|
||||
❌ Avoid: "email communication patterns", "API architecture philosophy"
|
||||
(These are abstract - search what user actually discussed!)
|
||||
|
||||
3. SEARCH: Execute your queries using searchMemory tool
|
||||
- Start with 2-3 core searches based on main entities/topics
|
||||
- Make each search specific and targeted
|
||||
- Use actual terms from the content, not rephrased concepts
|
||||
|
||||
4. OBSERVE: Evaluate search results
|
||||
- Did you find relevant episodes? How many unique ones?
|
||||
- What specific context emerged?
|
||||
- What new entities/topics appeared in results?
|
||||
- Are there gaps in understanding?
|
||||
- Should you search more angles?
|
||||
|
||||
Note: Episode counts are automatically deduplicated across searches - overlapping episodes are only counted once.
|
||||
|
||||
5. REACT: Decide next action based on observations
|
||||
|
||||
STOPPING CRITERIA - Proceed to SYNTHESIZE if ANY of these are true:
|
||||
- You found 20+ unique episodes across your searches → ENOUGH CONTEXT
|
||||
- You performed 5+ searches and found relevant episodes → SUFFICIENT
|
||||
- You performed 7+ searches regardless of results → EXHAUSTED STRATEGIES
|
||||
- You found strong relevant context from multiple angles → COMPLETE
|
||||
|
||||
System nudges will provide awareness of your progress, but you decide when synthesis quality would be optimal.
|
||||
|
||||
If you found little/no context AND searched less than 7 times:
|
||||
- Try different query angles from your decomposition
|
||||
- Search broader related topics
|
||||
- Search user's projects or work areas
|
||||
- Try alternative terminology
|
||||
|
||||
⚠️ DO NOT search endlessly - if you found relevant episodes, STOP and synthesize!
|
||||
|
||||
6. SYNTHESIZE: After gathering sufficient context, provide final answer
|
||||
- Wrap your synthesis in <final_response> tags
|
||||
- Present direct factual context from memory - no meta-commentary
|
||||
- Write as if providing background context to an AI assistant
|
||||
- Include: facts, decisions, preferences, patterns, timelines
|
||||
- Note any gaps, contradictions, or evolution in thinking
|
||||
- Keep it concise and actionable
|
||||
- DO NOT use phrases like "Previous discussions on", "From conversations", "Past preferences indicate"
|
||||
- DO NOT use conversational language like "you said" or "you mentioned"
|
||||
- Present information as direct factual statements
|
||||
|
||||
FINAL RESPONSE FORMAT:
|
||||
<final_response>
|
||||
[Direct synthesized context - factual statements only]
|
||||
|
||||
Good examples:
|
||||
- "The API redesign focuses on performance and scalability. Key decisions: moving to GraphQL, caching layer with Redis."
|
||||
- "Project Phoenix launches Q2 2024. Main features: real-time sync, offline mode, collaborative editing."
|
||||
- "Sarah leads the backend team. Recent work includes authentication refactor and database migration."
|
||||
|
||||
Bad examples:
|
||||
❌ "Previous discussions on the API revealed..."
|
||||
❌ "From past conversations, it appears that..."
|
||||
❌ "Past preferences indicate..."
|
||||
❌ "The user mentioned that..."
|
||||
|
||||
Just state the facts directly.
|
||||
</final_response>
|
||||
|
||||
${contextHints.length > 0 ? `\nCONTEXT HINTS:\n${contextHints.join("\n")}` : ""}
|
||||
|
||||
CRITICAL REQUIREMENTS:
|
||||
- ALWAYS start with DECOMPOSE step - extract entities, topics, temporal markers, actions
|
||||
- Form specific queries from your decomposition - use user's actual terms
|
||||
- Minimum 3 searches required
|
||||
- Maximum 10 searches allowed - must synthesize after that
|
||||
- STOP and synthesize when you hit stopping criteria (20+ episodes, 5+ searches with results, 7+ searches total)
|
||||
- Each search should target different aspects from decomposition
|
||||
- Present synthesis directly without meta-commentary
|
||||
|
||||
SEARCH QUALITY CHECKLIST:
|
||||
✅ Queries use specific terms from content (names, projects, exact phrases)
|
||||
✅ Searched multiple angles from decomposition (entities, topics, related areas)
|
||||
✅ Stop when you have enough unique context - don't search endlessly
|
||||
✅ Tried alternative terminology if initial searches found nothing
|
||||
❌ Avoid generic/abstract queries that don't match user's vocabulary
|
||||
❌ Don't stop at 3 searches if you found zero unique episodes
|
||||
❌ Don't keep searching when you already found 20+ unique episodes
|
||||
}`;
|
||||
}
|
||||
@ -14,7 +14,6 @@ import { env } from "~/env.server";
|
||||
import type { z } from "zod";
|
||||
import type { IngestBodyRequest } from "~/jobs/ingest/ingest-episode.logic";
|
||||
import type { CreateConversationTitlePayload } from "~/jobs/conversation/create-title.logic";
|
||||
import type { ProcessDeepSearchPayload } from "~/jobs/deep-search/deep-search.logic";
|
||||
import type { SessionCompactionPayload } from "~/jobs/session/session-compaction.logic";
|
||||
|
||||
type QueueProvider = "trigger" | "bullmq";
|
||||
@ -113,35 +112,6 @@ export async function enqueueCreateConversationTitle(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue deep search job
|
||||
*/
|
||||
export async function enqueueDeepSearch(
|
||||
payload: ProcessDeepSearchPayload,
|
||||
): Promise<{ id?: string }> {
|
||||
const provider = env.QUEUE_PROVIDER as QueueProvider;
|
||||
|
||||
if (provider === "trigger") {
|
||||
const { deepSearch } = await import("~/trigger/deep-search");
|
||||
const handler = await deepSearch.trigger({
|
||||
content: payload.content,
|
||||
userId: payload.userId,
|
||||
stream: true,
|
||||
metadata: payload.metadata,
|
||||
intentOverride: payload.intentOverride,
|
||||
});
|
||||
return { id: handler.id };
|
||||
} else {
|
||||
// BullMQ
|
||||
const { deepSearchQueue } = await import("~/bullmq/queues");
|
||||
const job = await deepSearchQueue.add("deep-search", payload, {
|
||||
attempts: 3,
|
||||
backoff: { type: "exponential", delay: 2000 },
|
||||
});
|
||||
return { id: job.id };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue session compaction job
|
||||
*/
|
||||
|
||||
@ -117,6 +117,7 @@ export function ErrorBoundary() {
|
||||
function App() {
|
||||
const { posthogProjectKey, telemetryEnabled } =
|
||||
useTypedLoaderData<typeof loader>();
|
||||
|
||||
usePostHog(posthogProjectKey, telemetryEnabled);
|
||||
const [theme] = useTheme();
|
||||
|
||||
|
||||
@ -1,44 +0,0 @@
|
||||
import { json } from "@remix-run/node";
|
||||
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
|
||||
|
||||
import { getWorkspaceByUser } from "~/models/workspace.server";
|
||||
import {
|
||||
createConversation,
|
||||
CreateConversationSchema,
|
||||
getCurrentConversationRun,
|
||||
readConversation,
|
||||
stopConversation,
|
||||
} from "~/services/conversation.server";
|
||||
import { z } from "zod";
|
||||
|
||||
export const ConversationIdSchema = z.object({
|
||||
conversationId: z.string(),
|
||||
});
|
||||
|
||||
const { action, loader } = createActionApiRoute(
|
||||
{
|
||||
params: ConversationIdSchema,
|
||||
allowJWT: true,
|
||||
authorization: {
|
||||
action: "oauth",
|
||||
},
|
||||
corsStrategy: "all",
|
||||
},
|
||||
async ({ authentication, params }) => {
|
||||
const workspace = await getWorkspaceByUser(authentication.userId);
|
||||
|
||||
if (!workspace) {
|
||||
throw new Error("No workspace found");
|
||||
}
|
||||
|
||||
// Call the service to get the redirect URL
|
||||
const run = await getCurrentConversationRun(
|
||||
params.conversationId,
|
||||
workspace?.id,
|
||||
);
|
||||
|
||||
return json(run);
|
||||
},
|
||||
);
|
||||
|
||||
export { action, loader };
|
||||
@ -1,41 +0,0 @@
|
||||
import { json } from "@remix-run/node";
|
||||
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
|
||||
|
||||
import { getWorkspaceByUser } from "~/models/workspace.server";
|
||||
import {
|
||||
createConversation,
|
||||
CreateConversationSchema,
|
||||
readConversation,
|
||||
stopConversation,
|
||||
} from "~/services/conversation.server";
|
||||
import { z } from "zod";
|
||||
|
||||
export const ConversationIdSchema = z.object({
|
||||
conversationId: z.string(),
|
||||
});
|
||||
|
||||
const { action, loader } = createActionApiRoute(
|
||||
{
|
||||
params: ConversationIdSchema,
|
||||
allowJWT: true,
|
||||
authorization: {
|
||||
action: "oauth",
|
||||
},
|
||||
corsStrategy: "all",
|
||||
method: "POST",
|
||||
},
|
||||
async ({ authentication, params }) => {
|
||||
const workspace = await getWorkspaceByUser(authentication.userId);
|
||||
|
||||
if (!workspace) {
|
||||
throw new Error("No workspace found");
|
||||
}
|
||||
|
||||
// Call the service to get the redirect URL
|
||||
const stop = await stopConversation(params.conversationId, workspace?.id);
|
||||
|
||||
return json(stop);
|
||||
},
|
||||
);
|
||||
|
||||
export { action, loader };
|
||||
@ -0,0 +1,45 @@
|
||||
// import { json } from "@remix-run/node";
|
||||
// import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
|
||||
// import { UI_MESSAGE_STREAM_HEADERS } from "ai";
|
||||
|
||||
// import { getConversationAndHistory } from "~/services/conversation.server";
|
||||
// import { z } from "zod";
|
||||
// import { createResumableStreamContext } from "resumable-stream";
|
||||
|
||||
// export const ConversationIdSchema = z.object({
|
||||
// conversationId: z.string(),
|
||||
// });
|
||||
|
||||
// const { action, loader } = createActionApiRoute(
|
||||
// {
|
||||
// params: ConversationIdSchema,
|
||||
// allowJWT: true,
|
||||
// authorization: {
|
||||
// action: "oauth",
|
||||
// },
|
||||
// corsStrategy: "all",
|
||||
// },
|
||||
// async ({ authentication, params }) => {
|
||||
// const conversation = await getConversationAndHistory(
|
||||
// params.conversationId,
|
||||
// authentication.userId,
|
||||
// );
|
||||
|
||||
// const lastConversation = conversation?.ConversationHistory.pop();
|
||||
|
||||
// if (!lastConversation) {
|
||||
// return json({}, { status: 204 });
|
||||
// }
|
||||
|
||||
// const streamContext = createResumableStreamContext({
|
||||
// waitUntil: null,
|
||||
// });
|
||||
|
||||
// return new Response(
|
||||
// await streamContext.resumeExistingStream(lastConversation.id),
|
||||
// { headers: UI_MESSAGE_STREAM_HEADERS },
|
||||
// );
|
||||
// },
|
||||
// );
|
||||
|
||||
// export { action, loader };
|
||||
@ -1,50 +0,0 @@
|
||||
import { json } from "@remix-run/node";
|
||||
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
|
||||
|
||||
import { getWorkspaceByUser } from "~/models/workspace.server";
|
||||
import {
|
||||
getConversation,
|
||||
deleteConversation,
|
||||
} from "~/services/conversation.server";
|
||||
import { z } from "zod";
|
||||
|
||||
export const ConversationIdSchema = z.object({
|
||||
conversationId: z.string(),
|
||||
});
|
||||
|
||||
const { action, loader } = createActionApiRoute(
|
||||
{
|
||||
params: ConversationIdSchema,
|
||||
allowJWT: true,
|
||||
authorization: {
|
||||
action: "oauth",
|
||||
},
|
||||
corsStrategy: "all",
|
||||
},
|
||||
async ({ params, authentication, request }) => {
|
||||
const workspace = await getWorkspaceByUser(authentication.userId);
|
||||
|
||||
if (!workspace) {
|
||||
throw new Error("No workspace found");
|
||||
}
|
||||
|
||||
const method = request.method;
|
||||
|
||||
if (method === "GET") {
|
||||
// Get a conversation by ID
|
||||
const conversation = await getConversation(params.conversationId);
|
||||
return json(conversation);
|
||||
}
|
||||
|
||||
if (method === "DELETE") {
|
||||
// Soft delete a conversation
|
||||
const deleted = await deleteConversation(params.conversationId);
|
||||
return json(deleted);
|
||||
}
|
||||
|
||||
// Method not allowed
|
||||
return new Response("Method Not Allowed", { status: 405 });
|
||||
},
|
||||
);
|
||||
|
||||
export { action, loader };
|
||||
@ -1,37 +1,152 @@
|
||||
import { json } from "@remix-run/node";
|
||||
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
|
||||
|
||||
import { getWorkspaceByUser } from "~/models/workspace.server";
|
||||
import {
|
||||
createConversation,
|
||||
CreateConversationSchema,
|
||||
convertToModelMessages,
|
||||
streamText,
|
||||
validateUIMessages,
|
||||
type LanguageModel,
|
||||
experimental_createMCPClient as createMCPClient,
|
||||
generateId,
|
||||
} from "ai";
|
||||
import { z } from "zod";
|
||||
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
||||
|
||||
import { createHybridActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
|
||||
import {
|
||||
createConversationHistory,
|
||||
getConversationAndHistory,
|
||||
} from "~/services/conversation.server";
|
||||
|
||||
const { action, loader } = createActionApiRoute(
|
||||
import { getModel } from "~/lib/model.server";
|
||||
import { UserTypeEnum } from "@core/types";
|
||||
import { nanoid } from "nanoid";
|
||||
import { getOrCreatePersonalAccessToken } from "~/services/personalAccessToken.server";
|
||||
import { REACT_SYSTEM_PROMPT } from "~/lib/prompt.server";
|
||||
import { enqueueCreateConversationTitle } from "~/lib/queue-adapter.server";
|
||||
import { env } from "~/env.server";
|
||||
|
||||
const ChatRequestSchema = z.object({
|
||||
message: z.object({
|
||||
id: z.string().optional(),
|
||||
parts: z.array(z.any()),
|
||||
role: z.string(),
|
||||
}),
|
||||
id: z.string(),
|
||||
});
|
||||
|
||||
const { loader, action } = createHybridActionApiRoute(
|
||||
{
|
||||
body: CreateConversationSchema,
|
||||
body: ChatRequestSchema,
|
||||
allowJWT: true,
|
||||
authorization: {
|
||||
action: "oauth",
|
||||
action: "conversation",
|
||||
},
|
||||
corsStrategy: "all",
|
||||
},
|
||||
async ({ body, authentication }) => {
|
||||
const workspace = await getWorkspaceByUser(authentication.userId);
|
||||
const randomKeyName = `chat_${nanoid(10)}`;
|
||||
const pat = await getOrCreatePersonalAccessToken({
|
||||
name: randomKeyName,
|
||||
userId: authentication.userId,
|
||||
});
|
||||
|
||||
if (!workspace) {
|
||||
throw new Error("No workspace found");
|
||||
const message = body.message.parts[0].text;
|
||||
const id = body.message.id;
|
||||
const apiEndpoint = `${env.APP_ORIGIN}/api/v1/mcp?source=core`;
|
||||
const url = new URL(apiEndpoint);
|
||||
|
||||
const mcpClient = await createMCPClient({
|
||||
transport: new StreamableHTTPClientTransport(url, {
|
||||
requestInit: {
|
||||
headers: pat.token
|
||||
? {
|
||||
Authorization: `Bearer ${pat.token}`,
|
||||
}
|
||||
: {},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
// Call the service to get the redirect URL
|
||||
const conversation = await createConversation(
|
||||
workspace?.id,
|
||||
const conversation = await getConversationAndHistory(
|
||||
body.id,
|
||||
authentication.userId,
|
||||
body,
|
||||
);
|
||||
|
||||
return json(conversation);
|
||||
const conversationHistory = conversation?.ConversationHistory ?? [];
|
||||
|
||||
if (conversationHistory.length === 0) {
|
||||
// Trigger conversation title task
|
||||
await enqueueCreateConversationTitle({
|
||||
conversationId: body.id,
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
if (conversationHistory.length > 1) {
|
||||
await createConversationHistory(message, body.id, UserTypeEnum.User);
|
||||
}
|
||||
|
||||
const messages = conversationHistory.map((history) => {
|
||||
return {
|
||||
parts: [{ text: history.message, type: "text" }],
|
||||
role: "user",
|
||||
id: history.id,
|
||||
};
|
||||
});
|
||||
|
||||
const tools = { ...(await mcpClient.tools()) };
|
||||
|
||||
// console.log(tools);
|
||||
|
||||
const finalMessages = [
|
||||
...messages,
|
||||
{
|
||||
parts: [{ text: message, type: "text" }],
|
||||
role: "user",
|
||||
id: id ?? generateId(),
|
||||
},
|
||||
];
|
||||
|
||||
const validatedMessages = await validateUIMessages({
|
||||
messages: finalMessages,
|
||||
});
|
||||
|
||||
const result = streamText({
|
||||
model: getModel() as LanguageModel,
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: REACT_SYSTEM_PROMPT,
|
||||
},
|
||||
...convertToModelMessages(validatedMessages),
|
||||
],
|
||||
tools,
|
||||
});
|
||||
|
||||
result.consumeStream(); // no await
|
||||
|
||||
return result.toUIMessageStreamResponse({
|
||||
originalMessages: validatedMessages,
|
||||
onFinish: async ({ messages }) => {
|
||||
console.log(JSON.stringify(messages));
|
||||
const lastMessage = messages.pop();
|
||||
let message = "";
|
||||
lastMessage?.parts.forEach((part) => {
|
||||
if (part.type === "text") {
|
||||
message += part.text;
|
||||
}
|
||||
});
|
||||
|
||||
await createConversationHistory(message, body.id, UserTypeEnum.Agent);
|
||||
},
|
||||
// async consumeSseStream({ stream }) {
|
||||
// // Create a resumable stream from the SSE stream
|
||||
// const streamContext = createResumableStreamContext({ waitUntil: null });
|
||||
// await streamContext.createNewResumableStream(
|
||||
// conversation.conversationHistoryId,
|
||||
// () => stream,
|
||||
// );
|
||||
// },
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
export { action, loader };
|
||||
export { loader, action };
|
||||
|
||||
@ -1,9 +1,26 @@
|
||||
import { z } from "zod";
|
||||
import { json } from "@remix-run/node";
|
||||
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
|
||||
import { enqueueDeepSearch } from "~/lib/queue-adapter.server";
|
||||
import { runs } from "@trigger.dev/sdk";
|
||||
import { trackFeatureUsage } from "~/services/telemetry.server";
|
||||
import { nanoid } from "nanoid";
|
||||
import {
|
||||
deletePersonalAccessToken,
|
||||
getOrCreatePersonalAccessToken,
|
||||
} from "~/services/personalAccessToken.server";
|
||||
|
||||
import {
|
||||
convertToModelMessages,
|
||||
type CoreMessage,
|
||||
generateText,
|
||||
type LanguageModel,
|
||||
streamText,
|
||||
tool,
|
||||
validateUIMessages,
|
||||
} from "ai";
|
||||
import axios from "axios";
|
||||
import { logger } from "~/services/logger.service";
|
||||
import { getReActPrompt } from "~/lib/prompt.server";
|
||||
import { getModel } from "~/lib/model.server";
|
||||
|
||||
const DeepSearchBodySchema = z.object({
|
||||
content: z.string().min(1, "Content is required"),
|
||||
@ -18,6 +35,48 @@ const DeepSearchBodySchema = z.object({
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export function createSearchMemoryTool(token: string) {
|
||||
return tool({
|
||||
description:
|
||||
"Search the user's memory for relevant facts and episodes. Use this tool multiple times with different queries to gather comprehensive context.",
|
||||
parameters: z.object({
|
||||
query: z
|
||||
.string()
|
||||
.describe(
|
||||
"Search query to find relevant information. Be specific: entity names, topics, concepts.",
|
||||
),
|
||||
}),
|
||||
execute: async ({ query }: { query: string }) => {
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${process.env.API_BASE_URL || "https://core.heysol.ai"}/api/v1/search`,
|
||||
{ query },
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const searchResult = response.data;
|
||||
|
||||
return {
|
||||
facts: searchResult.facts || [],
|
||||
episodes: searchResult.episodes || [],
|
||||
summary: `Found ${searchResult.episodes?.length || 0} relevant memories`,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`SearchMemory tool error: ${error}`);
|
||||
return {
|
||||
facts: [],
|
||||
episodes: [],
|
||||
summary: "No results found",
|
||||
};
|
||||
}
|
||||
},
|
||||
} as any);
|
||||
}
|
||||
|
||||
const { action, loader } = createActionApiRoute(
|
||||
{
|
||||
body: DeepSearchBodySchema,
|
||||
@ -30,37 +89,74 @@ const { action, loader } = createActionApiRoute(
|
||||
},
|
||||
async ({ body, authentication }) => {
|
||||
// Track deep search
|
||||
trackFeatureUsage("deep_search_performed", authentication.userId).catch(console.error);
|
||||
trackFeatureUsage("deep_search_performed", authentication.userId).catch(
|
||||
console.error,
|
||||
);
|
||||
|
||||
let trigger;
|
||||
if (!body.stream) {
|
||||
trigger = await enqueueDeepSearch({
|
||||
content: body.content,
|
||||
userId: authentication.userId,
|
||||
stream: body.stream,
|
||||
intentOverride: body.intentOverride,
|
||||
metadata: body.metadata,
|
||||
const randomKeyName = `deepSearch_${nanoid(10)}`;
|
||||
|
||||
const pat = await getOrCreatePersonalAccessToken({
|
||||
name: randomKeyName,
|
||||
userId: authentication.userId as string,
|
||||
});
|
||||
|
||||
return json(trigger);
|
||||
if (!pat?.token) {
|
||||
return json({
|
||||
success: false,
|
||||
error: "Failed to create personal access token",
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Create search tool that agent will use
|
||||
const searchTool = createSearchMemoryTool(pat.token);
|
||||
|
||||
const tools = {
|
||||
searchMemory: searchTool,
|
||||
};
|
||||
// Build initial messages with ReAct prompt
|
||||
const initialMessages: CoreMessage[] = [
|
||||
{
|
||||
role: "system",
|
||||
content: getReActPrompt(body.metadata, body.intentOverride),
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: `CONTENT TO ANALYZE:\n${body.content}\n\nPlease search my memory for relevant context and synthesize what you find.`,
|
||||
},
|
||||
];
|
||||
|
||||
const validatedMessages = await validateUIMessages({
|
||||
messages: initialMessages,
|
||||
tools,
|
||||
});
|
||||
|
||||
if (body.stream) {
|
||||
const result = streamText({
|
||||
model: getModel() as LanguageModel,
|
||||
messages: convertToModelMessages(validatedMessages),
|
||||
});
|
||||
|
||||
return result.toUIMessageStreamResponse({
|
||||
originalMessages: validatedMessages,
|
||||
});
|
||||
} else {
|
||||
const runHandler = await enqueueDeepSearch({
|
||||
content: body.content,
|
||||
userId: authentication.userId,
|
||||
stream: body.stream,
|
||||
intentOverride: body.intentOverride,
|
||||
metadata: body.metadata,
|
||||
const { text } = await generateText({
|
||||
model: getModel() as LanguageModel,
|
||||
messages: convertToModelMessages(validatedMessages),
|
||||
});
|
||||
|
||||
for await (const run of runs.subscribeToRun(runHandler.id)) {
|
||||
if (run.status === "COMPLETED") {
|
||||
return json(run.output);
|
||||
} else if (run.status === "FAILED") {
|
||||
return json(run.error);
|
||||
}
|
||||
await deletePersonalAccessToken(pat?.id);
|
||||
return json({ text });
|
||||
}
|
||||
} catch (error: any) {
|
||||
await deletePersonalAccessToken(pat?.id);
|
||||
logger.error(`Deep search error: ${error}`);
|
||||
|
||||
return json({ error: "Run failed" });
|
||||
return json({
|
||||
success: false,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@ -1,42 +1,26 @@
|
||||
import {
|
||||
type LoaderFunctionArgs,
|
||||
type ActionFunctionArgs,
|
||||
} from "@remix-run/server-runtime";
|
||||
import { sort } from "fast-sort";
|
||||
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
|
||||
import { useParams, useRevalidator, useNavigate } from "@remix-run/react";
|
||||
import { parse } from "@conform-to/zod";
|
||||
import {
|
||||
requireUserId,
|
||||
requireUser,
|
||||
requireWorkpace,
|
||||
} from "~/services/session.server";
|
||||
import {
|
||||
getConversationAndHistory,
|
||||
getCurrentConversationRun,
|
||||
stopConversation,
|
||||
createConversation,
|
||||
CreateConversationSchema,
|
||||
} from "~/services/conversation.server";
|
||||
import { type ConversationHistory } from "@core/database";
|
||||
import { useParams, useNavigate } from "@remix-run/react";
|
||||
import { requireUser, requireWorkpace } from "~/services/session.server";
|
||||
import { getConversationAndHistory } from "~/services/conversation.server";
|
||||
import {
|
||||
ConversationItem,
|
||||
ConversationTextarea,
|
||||
StreamingConversation,
|
||||
} from "~/components/conversation";
|
||||
import { useTypedLoaderData } from "remix-typedjson";
|
||||
import React from "react";
|
||||
import { ScrollAreaWithAutoScroll } from "~/components/use-auto-scroll";
|
||||
import { PageHeader } from "~/components/common/page-header";
|
||||
import { Plus } from "lucide-react";
|
||||
|
||||
import { json } from "@remix-run/node";
|
||||
import { env } from "~/env.server";
|
||||
import { type UIMessage, useChat } from "@ai-sdk/react";
|
||||
import { DefaultChatTransport } from "ai";
|
||||
import { UserTypeEnum } from "@core/types";
|
||||
import React from "react";
|
||||
|
||||
// Example loader accessing params
|
||||
export async function loader({ params, request }: LoaderFunctionArgs) {
|
||||
const user = await requireUser(request);
|
||||
const workspace = await requireWorkpace(request);
|
||||
|
||||
const conversation = await getConversationAndHistory(
|
||||
params.conversationId as string,
|
||||
user.id,
|
||||
@ -46,100 +30,37 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
|
||||
throw new Error("No conversation found");
|
||||
}
|
||||
|
||||
const run = await getCurrentConversationRun(conversation.id, workspace.id);
|
||||
|
||||
return { conversation, run, apiURL: env.TRIGGER_API_URL };
|
||||
}
|
||||
|
||||
// Example action accessing params
|
||||
export async function action({ params, request }: ActionFunctionArgs) {
|
||||
if (request.method.toUpperCase() !== "POST") {
|
||||
return new Response("Method Not Allowed", { status: 405 });
|
||||
}
|
||||
|
||||
const userId = await requireUserId(request);
|
||||
const workspace = await requireWorkpace(request);
|
||||
const formData = await request.formData();
|
||||
const { conversationId } = params;
|
||||
|
||||
if (!conversationId) {
|
||||
throw new Error("No conversation");
|
||||
}
|
||||
|
||||
// Check if this is a stop request (isLoading = true means stop button was clicked)
|
||||
const message = formData.get("message");
|
||||
|
||||
// If no message, it's a stop request
|
||||
if (!message) {
|
||||
const result = await stopConversation(conversationId, workspace.id);
|
||||
return json(result);
|
||||
}
|
||||
|
||||
// Otherwise, create a new conversation message
|
||||
const submission = parse(formData, { schema: CreateConversationSchema });
|
||||
|
||||
if (!submission.value || submission.intent !== "submit") {
|
||||
return json(submission);
|
||||
}
|
||||
|
||||
const conversation = await createConversation(workspace?.id, userId, {
|
||||
message: submission.value.message,
|
||||
title: submission.value.title,
|
||||
conversationId: submission.value.conversationId,
|
||||
});
|
||||
|
||||
return json({ conversation });
|
||||
return { conversation };
|
||||
}
|
||||
|
||||
// Accessing params in the component
|
||||
export default function SingleConversation() {
|
||||
const { conversation, run, apiURL } = useTypedLoaderData<typeof loader>();
|
||||
const conversationHistory = conversation.ConversationHistory;
|
||||
|
||||
const [conversationResponse, setConversationResponse] = React.useState<
|
||||
{ conversationHistoryId: string; id: string; token: string } | undefined
|
||||
>(run);
|
||||
|
||||
const { conversationId } = useParams();
|
||||
const revalidator = useRevalidator();
|
||||
const { conversation } = useTypedLoaderData<typeof loader>();
|
||||
const navigate = useNavigate();
|
||||
const { conversationId } = useParams();
|
||||
|
||||
const { sendMessage, messages, status, stop, regenerate } = useChat({
|
||||
id: conversationId, // use the provided chat ID
|
||||
messages: conversation.ConversationHistory.map(
|
||||
(history) =>
|
||||
({
|
||||
role: history.userType === UserTypeEnum.Agent ? "assistant" : "user",
|
||||
parts: [{ text: history.message, type: "text" }],
|
||||
}) as UIMessage,
|
||||
), // load initial messages
|
||||
transport: new DefaultChatTransport({
|
||||
api: "/api/v1/conversation",
|
||||
prepareSendMessagesRequest({ messages, id }) {
|
||||
return { body: { message: messages[messages.length - 1], id } };
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (run) {
|
||||
setConversationResponse(run);
|
||||
if (conversation.ConversationHistory.length === 1) {
|
||||
regenerate();
|
||||
}
|
||||
}, [run]);
|
||||
|
||||
const conversations = React.useMemo(() => {
|
||||
const lastConversationHistoryId =
|
||||
conversationResponse?.conversationHistoryId;
|
||||
|
||||
// First sort the conversation history by creation time
|
||||
const sortedConversationHistory = sort(conversationHistory).asc(
|
||||
(ch) => ch.createdAt,
|
||||
);
|
||||
|
||||
const lastIndex = sortedConversationHistory.findIndex(
|
||||
(item) => item.id === lastConversationHistoryId,
|
||||
);
|
||||
|
||||
// Filter out any conversation history items that come after the lastConversationHistoryId
|
||||
return lastConversationHistoryId
|
||||
? sortedConversationHistory.filter((_ch, currentIndex: number) => {
|
||||
return currentIndex <= lastIndex;
|
||||
})
|
||||
: sortedConversationHistory;
|
||||
}, [conversationResponse, conversationHistory]);
|
||||
|
||||
const getConversations = () => {
|
||||
return (
|
||||
<>
|
||||
{conversations.map((ch: ConversationHistory) => {
|
||||
return <ConversationItem key={ch.id} conversationHistory={ch} />;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (typeof window === "undefined") {
|
||||
return null;
|
||||
@ -166,41 +87,23 @@ export default function SingleConversation() {
|
||||
<div className="relative flex h-[calc(100vh_-_56px)] w-full flex-col items-center justify-center overflow-auto">
|
||||
<div className="flex h-[calc(100vh_-_80px)] w-full flex-col justify-end overflow-hidden">
|
||||
<ScrollAreaWithAutoScroll>
|
||||
{getConversations()}
|
||||
{conversationResponse && (
|
||||
<StreamingConversation
|
||||
runId={conversationResponse.id}
|
||||
token={conversationResponse.token}
|
||||
afterStreaming={() => {
|
||||
setConversationResponse(undefined);
|
||||
revalidator.revalidate();
|
||||
}}
|
||||
apiURL={apiURL}
|
||||
/>
|
||||
)}
|
||||
{messages.map((message: UIMessage, index: number) => {
|
||||
return <ConversationItem key={index} message={message} />;
|
||||
})}
|
||||
</ScrollAreaWithAutoScroll>
|
||||
|
||||
<div className="flex w-full flex-col items-center">
|
||||
<div className="w-full max-w-[80ch] px-1 pr-2">
|
||||
{conversation?.status !== "need_approval" && (
|
||||
<ConversationTextarea
|
||||
conversationId={conversationId as string}
|
||||
className="bg-background-3 w-full border-1 border-gray-300"
|
||||
isLoading={
|
||||
!!conversationResponse || conversation?.status === "running"
|
||||
}
|
||||
onConversationCreated={(conversation) => {
|
||||
if (conversation) {
|
||||
setConversationResponse({
|
||||
conversationHistoryId:
|
||||
conversation.conversationHistoryId,
|
||||
id: conversation.id,
|
||||
token: conversation.token,
|
||||
});
|
||||
isLoading={status === "streaming"}
|
||||
onConversationCreated={(message) => {
|
||||
if (message) {
|
||||
sendMessage({ text: message });
|
||||
}
|
||||
}}
|
||||
stop={() => stop()}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -43,8 +43,7 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
|
||||
const conversation = await createConversation(workspace?.id, userId, {
|
||||
message: submission.value.message,
|
||||
title: submission.value.title,
|
||||
conversationId: submission.value.conversationId,
|
||||
title: submission.value.title ?? "Untitled",
|
||||
});
|
||||
|
||||
// If conversationId exists in submission, return the conversation data (don't redirect)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,11 +1,8 @@
|
||||
import { UserTypeEnum } from "@core/types";
|
||||
|
||||
import { auth, runs, tasks } from "@trigger.dev/sdk/v3";
|
||||
import { prisma } from "~/db.server";
|
||||
import { enqueueCreateConversationTitle } from "~/lib/queue-adapter.server";
|
||||
|
||||
import { z } from "zod";
|
||||
import { type ConversationHistory } from "@prisma/client";
|
||||
import { trackFeatureUsage } from "~/services/telemetry.server";
|
||||
|
||||
export const CreateConversationSchema = z.object({
|
||||
@ -45,23 +42,10 @@ export async function createConversation(
|
||||
},
|
||||
});
|
||||
|
||||
const context = await getConversationContext(conversationHistory.id);
|
||||
const handler = await tasks.trigger(
|
||||
"chat",
|
||||
{
|
||||
conversationHistoryId: conversationHistory.id,
|
||||
conversationId: conversationHistory.conversation.id,
|
||||
context,
|
||||
},
|
||||
{ tags: [conversationHistory.id, workspaceId, conversationId] },
|
||||
);
|
||||
|
||||
// Track conversation message
|
||||
trackFeatureUsage("conversation_message_sent", userId).catch(console.error);
|
||||
|
||||
return {
|
||||
id: handler.id,
|
||||
token: handler.publicAccessToken,
|
||||
conversationId: conversationHistory.conversation.id,
|
||||
conversationHistoryId: conversationHistory.id,
|
||||
};
|
||||
@ -88,39 +72,20 @@ export async function createConversation(
|
||||
});
|
||||
|
||||
const conversationHistory = conversation.ConversationHistory[0];
|
||||
const context = await getConversationContext(conversationHistory.id);
|
||||
|
||||
// Trigger conversation title task
|
||||
await enqueueCreateConversationTitle({
|
||||
conversationId: conversation.id,
|
||||
message: conversationData.message,
|
||||
});
|
||||
|
||||
const handler = await tasks.trigger(
|
||||
"chat",
|
||||
{
|
||||
conversationHistoryId: conversationHistory.id,
|
||||
conversationId: conversation.id,
|
||||
context,
|
||||
},
|
||||
{ tags: [conversationHistory.id, workspaceId, conversation.id] },
|
||||
);
|
||||
|
||||
// Track new conversation creation
|
||||
trackFeatureUsage("conversation_created", userId).catch(console.error);
|
||||
|
||||
return {
|
||||
id: handler.id,
|
||||
token: handler.publicAccessToken,
|
||||
conversationId: conversation.id,
|
||||
conversationHistoryId: conversationHistory.id,
|
||||
};
|
||||
}
|
||||
|
||||
// Get a conversation by ID
|
||||
export async function getConversation(conversationId: string) {
|
||||
export async function getConversation(conversationId: string, userId: string) {
|
||||
return prisma.conversation.findUnique({
|
||||
where: { id: conversationId },
|
||||
where: { id: conversationId, userId },
|
||||
});
|
||||
}
|
||||
|
||||
@ -142,141 +107,6 @@ export async function readConversation(conversationId: string) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function getCurrentConversationRun(
|
||||
conversationId: string,
|
||||
workspaceId: string,
|
||||
) {
|
||||
const conversationHistory = await prisma.conversationHistory.findFirst({
|
||||
where: {
|
||||
conversationId,
|
||||
conversation: {
|
||||
workspaceId,
|
||||
},
|
||||
userType: UserTypeEnum.User,
|
||||
},
|
||||
orderBy: {
|
||||
updatedAt: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
if (!conversationHistory) {
|
||||
throw new Error("No run found");
|
||||
}
|
||||
|
||||
const response = await runs.list({
|
||||
tag: [conversationId, conversationHistory.id, workspaceId],
|
||||
status: ["QUEUED", "EXECUTING"],
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
if (!response) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const run = response?.data?.[0];
|
||||
|
||||
if (!run) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const publicToken = await auth.createPublicToken({
|
||||
scopes: {
|
||||
read: {
|
||||
runs: [run.id],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: run.id,
|
||||
token: publicToken,
|
||||
conversationId,
|
||||
conversationHistoryId: conversationHistory.id,
|
||||
};
|
||||
}
|
||||
|
||||
export async function stopConversation(
|
||||
conversationId: string,
|
||||
workspaceId: string,
|
||||
) {
|
||||
const conversationHistory = await prisma.conversationHistory.findFirst({
|
||||
where: {
|
||||
conversationId,
|
||||
conversation: {
|
||||
workspaceId,
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
updatedAt: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
if (!conversationHistory) {
|
||||
throw new Error("No run found");
|
||||
}
|
||||
|
||||
const response = await runs.list({
|
||||
tag: [conversationId, conversationHistory.id],
|
||||
status: ["QUEUED", "EXECUTING"],
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
const run = response.data[0];
|
||||
if (!run) {
|
||||
await prisma.conversation.update({
|
||||
where: {
|
||||
id: conversationId,
|
||||
},
|
||||
data: {
|
||||
status: "failed",
|
||||
},
|
||||
});
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return await runs.cancel(run.id);
|
||||
}
|
||||
|
||||
export async function getConversationContext(
|
||||
conversationHistoryId: string,
|
||||
): Promise<{
|
||||
previousHistory: ConversationHistory[];
|
||||
}> {
|
||||
const conversationHistory = await prisma.conversationHistory.findUnique({
|
||||
where: { id: conversationHistoryId },
|
||||
include: { conversation: true },
|
||||
});
|
||||
|
||||
if (!conversationHistory) {
|
||||
return {
|
||||
previousHistory: [],
|
||||
};
|
||||
}
|
||||
|
||||
// Get previous conversation history message and response
|
||||
let previousHistory: ConversationHistory[] = [];
|
||||
|
||||
if (conversationHistory.conversationId) {
|
||||
previousHistory = await prisma.conversationHistory.findMany({
|
||||
where: {
|
||||
conversationId: conversationHistory.conversationId,
|
||||
id: {
|
||||
not: conversationHistoryId,
|
||||
},
|
||||
deleted: null,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "asc",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
previousHistory,
|
||||
};
|
||||
}
|
||||
|
||||
export const getConversationAndHistory = async (
|
||||
conversationId: string,
|
||||
userId: string,
|
||||
@ -284,6 +114,7 @@ export const getConversationAndHistory = async (
|
||||
const conversation = await prisma.conversation.findFirst({
|
||||
where: {
|
||||
id: conversationId,
|
||||
userId,
|
||||
},
|
||||
include: {
|
||||
ConversationHistory: true,
|
||||
@ -293,6 +124,23 @@ export const getConversationAndHistory = async (
|
||||
return conversation;
|
||||
};
|
||||
|
||||
export const createConversationHistory = async (
|
||||
userMessage: string,
|
||||
conversationId: string,
|
||||
userType: UserTypeEnum,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
thoughts?: Record<string, any>,
|
||||
) => {
|
||||
return await prisma.conversationHistory.create({
|
||||
data: {
|
||||
conversationId,
|
||||
message: userMessage,
|
||||
thoughts,
|
||||
userType,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const GetConversationsListSchema = z.object({
|
||||
page: z.string().optional().default("1"),
|
||||
limit: z.string().optional().default("20"),
|
||||
|
||||
@ -10,7 +10,6 @@ import {
|
||||
type EpisodeType,
|
||||
} from "@core/types";
|
||||
import { logger } from "./logger.service";
|
||||
import { ClusteringService } from "./clustering.server";
|
||||
import crypto from "crypto";
|
||||
import { dedupeNodes, extractEntities } from "./prompts/nodes";
|
||||
import {
|
||||
@ -50,12 +49,6 @@ import { type PrismaClient } from "@prisma/client";
|
||||
const DEFAULT_EPISODE_WINDOW = 5;
|
||||
|
||||
export class KnowledgeGraphService {
|
||||
private clusteringService: ClusteringService;
|
||||
|
||||
constructor() {
|
||||
this.clusteringService = new ClusteringService();
|
||||
}
|
||||
|
||||
async getEmbedding(text: string) {
|
||||
return getEmbedding(text);
|
||||
}
|
||||
@ -564,9 +557,9 @@ export class KnowledgeGraphService {
|
||||
(text, _model, usage) => {
|
||||
responseText = text;
|
||||
if (usage) {
|
||||
tokenMetrics.high.input += usage.promptTokens;
|
||||
tokenMetrics.high.output += usage.completionTokens;
|
||||
tokenMetrics.high.total += usage.totalTokens;
|
||||
tokenMetrics.high.input += usage.promptTokens as number;
|
||||
tokenMetrics.high.output += usage.completionTokens as number;
|
||||
tokenMetrics.high.total += usage.totalTokens as number;
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
|
||||
@ -320,6 +320,14 @@ export async function getOrCreatePersonalAccessToken({
|
||||
};
|
||||
}
|
||||
|
||||
export async function deletePersonalAccessToken(tokenId: string) {
|
||||
return await prisma.personalAccessToken.delete({
|
||||
where: {
|
||||
id: tokenId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Created a new PersonalAccessToken, and return the token. We only ever return the unencrypted token once. */
|
||||
export async function createPersonalAccessToken({
|
||||
name,
|
||||
|
||||
@ -1,492 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { ActionStatusEnum } from "@core/types";
|
||||
import { logger } from "@trigger.dev/sdk/v3";
|
||||
import {
|
||||
type CoreMessage,
|
||||
type DataContent,
|
||||
jsonSchema,
|
||||
tool,
|
||||
type ToolSet,
|
||||
} from "ai";
|
||||
import axios from "axios";
|
||||
import Handlebars from "handlebars";
|
||||
|
||||
import { REACT_SYSTEM_PROMPT, REACT_USER_PROMPT } from "./prompt";
|
||||
import { generate, processTag } from "./stream-utils";
|
||||
import { type AgentMessage, AgentMessageType, Message } from "./types";
|
||||
import { type MCP } from "../utils/mcp";
|
||||
import {
|
||||
type ExecutionState,
|
||||
type HistoryStep,
|
||||
type Resource,
|
||||
type TotalCost,
|
||||
} from "../utils/types";
|
||||
import { flattenObject } from "../utils/utils";
|
||||
|
||||
interface LLMOutputInterface {
|
||||
response: AsyncGenerator<
|
||||
| string
|
||||
| {
|
||||
type: string;
|
||||
toolName: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
args?: any;
|
||||
toolCallId?: string;
|
||||
message?: string;
|
||||
},
|
||||
any,
|
||||
any
|
||||
>;
|
||||
}
|
||||
|
||||
const progressUpdateTool = tool({
|
||||
description:
|
||||
"Send a progress update to the user about what has been discovered or will be done next in a crisp and user friendly way no technical terms",
|
||||
parameters: jsonSchema({
|
||||
type: "object",
|
||||
properties: {
|
||||
message: {
|
||||
type: "string",
|
||||
description: "The progress update message to send to the user",
|
||||
},
|
||||
},
|
||||
required: ["message"],
|
||||
additionalProperties: false,
|
||||
}),
|
||||
});
|
||||
|
||||
const internalTools = ["core--progress_update"];
|
||||
|
||||
async function addResources(messages: CoreMessage[], resources: Resource[]) {
|
||||
const resourcePromises = resources.map(async (resource) => {
|
||||
// Remove everything before "/api" in the publicURL
|
||||
if (resource.publicURL) {
|
||||
const apiIndex = resource.publicURL.indexOf("/api");
|
||||
if (apiIndex !== -1) {
|
||||
resource.publicURL = resource.publicURL.substring(apiIndex);
|
||||
}
|
||||
}
|
||||
const response = await axios.get(resource.publicURL, {
|
||||
responseType: "arraybuffer",
|
||||
});
|
||||
|
||||
if (resource.fileType.startsWith("image/")) {
|
||||
return {
|
||||
type: "image",
|
||||
image: response.data as DataContent,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: "file",
|
||||
data: response.data as DataContent,
|
||||
|
||||
mimeType: resource.fileType,
|
||||
};
|
||||
});
|
||||
|
||||
const content = await Promise.all(resourcePromises);
|
||||
|
||||
return [...messages, { role: "user", content } as CoreMessage];
|
||||
}
|
||||
|
||||
function toolToMessage(history: HistoryStep[], messages: CoreMessage[]) {
|
||||
for (let i = 0; i < history.length; i++) {
|
||||
const step = history[i];
|
||||
|
||||
// Add assistant message with tool calls
|
||||
if (step.observation && step.skillId) {
|
||||
messages.push({
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool-call",
|
||||
toolCallId: step.skillId,
|
||||
toolName: step.skill ?? "",
|
||||
args:
|
||||
typeof step.skillInput === "string"
|
||||
? JSON.parse(step.skillInput)
|
||||
: step.skillInput,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
messages.push({
|
||||
role: "tool",
|
||||
content: [
|
||||
{
|
||||
type: "tool-result",
|
||||
toolName: step.skill,
|
||||
toolCallId: step.skillId,
|
||||
result: step.observation,
|
||||
isError: step.isError,
|
||||
},
|
||||
],
|
||||
} as any);
|
||||
}
|
||||
// Handle format correction steps (observation exists but no skillId)
|
||||
else if (step.observation && !step.skillId) {
|
||||
// Add as a system message for format correction
|
||||
messages.push({
|
||||
role: "system",
|
||||
content: step.observation,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
async function makeNextCall(
|
||||
executionState: ExecutionState,
|
||||
TOOLS: ToolSet,
|
||||
totalCost: TotalCost,
|
||||
guardLoop: number,
|
||||
): Promise<LLMOutputInterface> {
|
||||
const { context, history, previousHistory } = executionState;
|
||||
|
||||
const promptInfo = {
|
||||
USER_MESSAGE: executionState.query,
|
||||
CONTEXT: context,
|
||||
USER_MEMORY: executionState.userMemoryContext,
|
||||
};
|
||||
|
||||
let messages: CoreMessage[] = [];
|
||||
|
||||
const systemTemplateHandler = Handlebars.compile(REACT_SYSTEM_PROMPT);
|
||||
let systemPrompt = systemTemplateHandler(promptInfo);
|
||||
|
||||
const userTemplateHandler = Handlebars.compile(REACT_USER_PROMPT);
|
||||
const userPrompt = userTemplateHandler(promptInfo);
|
||||
|
||||
// Always start with a system message (this does use tokens but keeps the instructions clear)
|
||||
messages.push({ role: "system", content: systemPrompt });
|
||||
|
||||
// For subsequent queries, include only final responses from previous exchanges if available
|
||||
if (previousHistory && previousHistory.length > 0) {
|
||||
messages = [...messages, ...previousHistory];
|
||||
}
|
||||
|
||||
// Add the current user query (much simpler than the full prompt)
|
||||
messages.push({ role: "user", content: userPrompt });
|
||||
|
||||
// Include any steps from the current interaction
|
||||
if (history.length > 0) {
|
||||
messages = toolToMessage(history, messages);
|
||||
}
|
||||
|
||||
if (executionState.resources && executionState.resources.length > 0) {
|
||||
messages = await addResources(messages, executionState.resources);
|
||||
}
|
||||
|
||||
// Get the next action from the LLM
|
||||
const response = generate(
|
||||
messages,
|
||||
guardLoop > 0 && guardLoop % 3 === 0,
|
||||
(event) => {
|
||||
const usage = event.usage;
|
||||
totalCost.inputTokens += usage.promptTokens;
|
||||
totalCost.outputTokens += usage.completionTokens;
|
||||
},
|
||||
TOOLS,
|
||||
);
|
||||
|
||||
return { response };
|
||||
}
|
||||
|
||||
export async function* run(
|
||||
message: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
context: Record<string, any>,
|
||||
previousHistory: CoreMessage[],
|
||||
mcp: MCP,
|
||||
stepHistory: HistoryStep[],
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
): AsyncGenerator<AgentMessage, any, any> {
|
||||
let guardLoop = 0;
|
||||
|
||||
let tools = {
|
||||
...(await mcp.allTools()),
|
||||
"core--progress_update": progressUpdateTool,
|
||||
};
|
||||
|
||||
logger.info("Tools have been formed");
|
||||
|
||||
let contextText = "";
|
||||
let resources = [];
|
||||
if (context) {
|
||||
// Extract resources and remove from context
|
||||
resources = context.resources || [];
|
||||
delete context.resources;
|
||||
|
||||
// Process remaining context
|
||||
contextText = flattenObject(context).join("\n");
|
||||
}
|
||||
|
||||
const executionState: ExecutionState = {
|
||||
query: message,
|
||||
context: contextText,
|
||||
resources,
|
||||
previousHistory,
|
||||
history: stepHistory, // Track the full ReAct history
|
||||
completed: false,
|
||||
};
|
||||
|
||||
const totalCost: TotalCost = { inputTokens: 0, outputTokens: 0, cost: 0 };
|
||||
|
||||
try {
|
||||
while (!executionState.completed && guardLoop < 50) {
|
||||
logger.info(`Starting the loop: ${guardLoop}`);
|
||||
|
||||
const { response: llmResponse } = await makeNextCall(
|
||||
executionState,
|
||||
tools,
|
||||
totalCost,
|
||||
guardLoop,
|
||||
);
|
||||
|
||||
let toolCallInfo;
|
||||
|
||||
const messageState = {
|
||||
inTag: false,
|
||||
message: "",
|
||||
messageEnded: false,
|
||||
lastSent: "",
|
||||
};
|
||||
|
||||
const questionState = {
|
||||
inTag: false,
|
||||
message: "",
|
||||
messageEnded: false,
|
||||
lastSent: "",
|
||||
};
|
||||
|
||||
let totalMessage = "";
|
||||
const toolCalls = [];
|
||||
|
||||
// LLM thought response
|
||||
for await (const chunk of llmResponse) {
|
||||
if (typeof chunk === "object" && chunk.type === "tool-call") {
|
||||
toolCallInfo = chunk;
|
||||
toolCalls.push(chunk);
|
||||
}
|
||||
|
||||
totalMessage += chunk;
|
||||
|
||||
if (!messageState.messageEnded) {
|
||||
yield* processTag(
|
||||
messageState,
|
||||
totalMessage,
|
||||
chunk as string,
|
||||
"<final_response>",
|
||||
"</final_response>",
|
||||
{
|
||||
start: AgentMessageType.MESSAGE_START,
|
||||
chunk: AgentMessageType.MESSAGE_CHUNK,
|
||||
end: AgentMessageType.MESSAGE_END,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (!questionState.messageEnded) {
|
||||
yield* processTag(
|
||||
questionState,
|
||||
totalMessage,
|
||||
chunk as string,
|
||||
"<question_response>",
|
||||
"</question_response>",
|
||||
{
|
||||
start: AgentMessageType.MESSAGE_START,
|
||||
chunk: AgentMessageType.MESSAGE_CHUNK,
|
||||
end: AgentMessageType.MESSAGE_END,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Cost for thought: ${JSON.stringify(totalCost)}`);
|
||||
|
||||
// Replace the error-handling block with this self-correcting implementation
|
||||
if (
|
||||
!totalMessage.includes("final_response") &&
|
||||
!totalMessage.includes("question_response") &&
|
||||
!toolCallInfo
|
||||
) {
|
||||
// Log the issue for debugging
|
||||
logger.info(
|
||||
`Invalid response format detected. Attempting to get proper format.`,
|
||||
);
|
||||
|
||||
// Extract the raw content from the invalid response
|
||||
const rawContent = totalMessage
|
||||
.replace(/(<[^>]*>|<\/[^>]*>)/g, "")
|
||||
.trim();
|
||||
|
||||
// Create a correction step
|
||||
const stepRecord: HistoryStep = {
|
||||
thought: "",
|
||||
skill: "",
|
||||
skillId: "",
|
||||
userMessage: "Core agent error, retrying \n",
|
||||
isQuestion: false,
|
||||
isFinal: false,
|
||||
tokenCount: totalCost,
|
||||
skillInput: "",
|
||||
observation: `Your last response was not in a valid format. You must respond with EXACTLY ONE of the required formats: either a tool call, <question_response> tags, or <final_response> tags. Please reformat your previous response using the correct format:\n\n${rawContent}`,
|
||||
};
|
||||
|
||||
yield Message("", AgentMessageType.MESSAGE_START);
|
||||
yield Message(
|
||||
stepRecord.userMessage as string,
|
||||
AgentMessageType.MESSAGE_CHUNK,
|
||||
);
|
||||
yield Message("", AgentMessageType.MESSAGE_END);
|
||||
|
||||
// Add this step to the history
|
||||
yield Message(JSON.stringify(stepRecord), AgentMessageType.STEP);
|
||||
executionState.history.push(stepRecord);
|
||||
|
||||
// Log that we're continuing the loop with a correction request
|
||||
logger.info(`Added format correction request to history.`);
|
||||
|
||||
// Don't mark as completed - let the loop continue
|
||||
guardLoop++; // Still increment to prevent infinite loops
|
||||
continue;
|
||||
}
|
||||
|
||||
// Record this step in history
|
||||
const stepRecord: HistoryStep = {
|
||||
thought: "",
|
||||
skill: "",
|
||||
skillId: "",
|
||||
userMessage: "",
|
||||
isQuestion: false,
|
||||
isFinal: false,
|
||||
tokenCount: totalCost,
|
||||
skillInput: "",
|
||||
};
|
||||
|
||||
if (totalMessage && totalMessage.includes("final_response")) {
|
||||
executionState.completed = true;
|
||||
stepRecord.isFinal = true;
|
||||
stepRecord.userMessage = messageState.message;
|
||||
stepRecord.finalTokenCount = totalCost;
|
||||
stepRecord.skillStatus = ActionStatusEnum.SUCCESS;
|
||||
yield Message(JSON.stringify(stepRecord), AgentMessageType.STEP);
|
||||
executionState.history.push(stepRecord);
|
||||
break;
|
||||
}
|
||||
|
||||
if (totalMessage && totalMessage.includes("question_response")) {
|
||||
executionState.completed = true;
|
||||
stepRecord.isQuestion = true;
|
||||
stepRecord.userMessage = questionState.message;
|
||||
stepRecord.finalTokenCount = totalCost;
|
||||
stepRecord.skillStatus = ActionStatusEnum.QUESTION;
|
||||
yield Message(JSON.stringify(stepRecord), AgentMessageType.STEP);
|
||||
executionState.history.push(stepRecord);
|
||||
break;
|
||||
}
|
||||
|
||||
if (toolCalls && toolCalls.length > 0) {
|
||||
// Run all tool calls in parallel
|
||||
for (const toolCallInfo of toolCalls) {
|
||||
const skillName = toolCallInfo.toolName;
|
||||
const skillId = toolCallInfo.toolCallId;
|
||||
const skillInput = toolCallInfo.args;
|
||||
|
||||
const toolName = skillName.split("--")[1];
|
||||
const agent = skillName.split("--")[0];
|
||||
|
||||
const stepRecord: HistoryStep = {
|
||||
agent,
|
||||
thought: "",
|
||||
skill: skillName,
|
||||
skillId,
|
||||
userMessage: "",
|
||||
isQuestion: false,
|
||||
isFinal: false,
|
||||
tokenCount: totalCost,
|
||||
skillInput: JSON.stringify(skillInput),
|
||||
};
|
||||
|
||||
if (!internalTools.includes(skillName)) {
|
||||
const skillMessageToSend = `\n<skill id="${skillId}" name="${toolName}" agent="${agent}"></skill>\n`;
|
||||
|
||||
stepRecord.userMessage += skillMessageToSend;
|
||||
|
||||
yield Message("", AgentMessageType.MESSAGE_START);
|
||||
yield Message(skillMessageToSend, AgentMessageType.MESSAGE_CHUNK);
|
||||
yield Message("", AgentMessageType.MESSAGE_END);
|
||||
}
|
||||
|
||||
let result;
|
||||
try {
|
||||
// Log skill execution details
|
||||
logger.info(`Executing skill: ${skillName}`);
|
||||
logger.info(`Input parameters: ${JSON.stringify(skillInput)}`);
|
||||
|
||||
if (!internalTools.includes(toolName)) {
|
||||
yield Message(
|
||||
JSON.stringify({ skillId, status: "start" }),
|
||||
AgentMessageType.SKILL_START,
|
||||
);
|
||||
}
|
||||
|
||||
// Handle CORE agent tools
|
||||
if (agent === "core") {
|
||||
if (toolName === "progress_update") {
|
||||
yield Message("", AgentMessageType.MESSAGE_START);
|
||||
yield Message(
|
||||
skillInput.message,
|
||||
AgentMessageType.MESSAGE_CHUNK,
|
||||
);
|
||||
stepRecord.userMessage += skillInput.message;
|
||||
yield Message("", AgentMessageType.MESSAGE_END);
|
||||
result = "Progress update sent successfully";
|
||||
}
|
||||
}
|
||||
// Handle other MCP tools
|
||||
else {
|
||||
result = await mcp.callTool(skillName, skillInput);
|
||||
|
||||
yield Message(
|
||||
JSON.stringify({ result, skillId }),
|
||||
AgentMessageType.SKILL_CHUNK,
|
||||
);
|
||||
}
|
||||
|
||||
yield Message(
|
||||
JSON.stringify({ skillId, status: "end" }),
|
||||
AgentMessageType.SKILL_END,
|
||||
);
|
||||
|
||||
stepRecord.skillOutput =
|
||||
typeof result === "object"
|
||||
? JSON.stringify(result, null, 2)
|
||||
: result;
|
||||
stepRecord.observation = stepRecord.skillOutput;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
logger.error(e as string);
|
||||
stepRecord.skillInput = skillInput;
|
||||
stepRecord.observation = JSON.stringify(e);
|
||||
stepRecord.isError = true;
|
||||
}
|
||||
|
||||
logger.info(`Skill step: ${JSON.stringify(stepRecord)}`);
|
||||
|
||||
yield Message(JSON.stringify(stepRecord), AgentMessageType.STEP);
|
||||
executionState.history.push(stepRecord);
|
||||
}
|
||||
}
|
||||
guardLoop++;
|
||||
}
|
||||
yield Message("Stream ended", AgentMessageType.STREAM_END);
|
||||
} catch (e) {
|
||||
logger.error(e as string);
|
||||
yield Message((e as Error).message, AgentMessageType.ERROR);
|
||||
yield Message("Stream ended", AgentMessageType.STREAM_END);
|
||||
}
|
||||
}
|
||||
@ -1,150 +0,0 @@
|
||||
import { ActionStatusEnum } from "@core/types";
|
||||
import { metadata, task, queue } from "@trigger.dev/sdk";
|
||||
|
||||
import { run } from "./chat-utils";
|
||||
import { MCP } from "../utils/mcp";
|
||||
import { type HistoryStep } from "../utils/types";
|
||||
import {
|
||||
createConversationHistoryForAgent,
|
||||
deductCredits,
|
||||
deletePersonalAccessToken,
|
||||
getPreviousExecutionHistory,
|
||||
hasCredits,
|
||||
InsufficientCreditsError,
|
||||
init,
|
||||
type RunChatPayload,
|
||||
updateConversationHistoryMessage,
|
||||
updateConversationStatus,
|
||||
updateExecutionStep,
|
||||
} from "../utils/utils";
|
||||
|
||||
const chatQueue = queue({
|
||||
name: "chat-queue",
|
||||
concurrencyLimit: 50,
|
||||
});
|
||||
|
||||
/**
|
||||
* Main chat task that orchestrates the agent workflow
|
||||
* Handles conversation context, agent selection, and LLM interactions
|
||||
*/
|
||||
export const chat = task({
|
||||
id: "chat",
|
||||
maxDuration: 3000,
|
||||
queue: chatQueue,
|
||||
init,
|
||||
run: async (payload: RunChatPayload, { init }) => {
|
||||
await updateConversationStatus("running", payload.conversationId);
|
||||
|
||||
try {
|
||||
// Check if workspace has sufficient credits before processing
|
||||
if (init?.conversation.workspaceId) {
|
||||
const hasSufficientCredits = await hasCredits(
|
||||
init.conversation.workspaceId,
|
||||
"chatMessage",
|
||||
);
|
||||
|
||||
if (!hasSufficientCredits) {
|
||||
throw new InsufficientCreditsError(
|
||||
"Insufficient credits to process chat message. Please upgrade your plan or wait for your credits to reset.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const { previousHistory, ...otherData } = payload.context;
|
||||
|
||||
// Initialise mcp
|
||||
const mcpHeaders = { Authorization: `Bearer ${init?.token}` };
|
||||
const mcp = new MCP();
|
||||
await mcp.init();
|
||||
await mcp.load(mcpHeaders);
|
||||
|
||||
// Prepare context with additional metadata
|
||||
const context = {
|
||||
// Currently this is assuming we only have one page in context
|
||||
context: {
|
||||
...(otherData.page && otherData.page.length > 0
|
||||
? { page: otherData.page[0] }
|
||||
: {}),
|
||||
},
|
||||
workpsaceId: init?.conversation.workspaceId,
|
||||
resources: otherData.resources,
|
||||
todayDate: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Extract user's goal from conversation history
|
||||
const message = init?.conversationHistory?.message;
|
||||
// Retrieve execution history from previous interactions
|
||||
const previousExecutionHistory = getPreviousExecutionHistory(
|
||||
previousHistory ?? [],
|
||||
);
|
||||
|
||||
let agentUserMessage = "";
|
||||
let agentConversationHistory;
|
||||
let stepHistory: HistoryStep[] = [];
|
||||
// Prepare conversation history in agent-compatible format
|
||||
agentConversationHistory = await createConversationHistoryForAgent(
|
||||
payload.conversationId,
|
||||
);
|
||||
|
||||
const llmResponse = run(
|
||||
message as string,
|
||||
context,
|
||||
previousExecutionHistory,
|
||||
mcp,
|
||||
stepHistory,
|
||||
);
|
||||
|
||||
const stream = await metadata.stream("messages", llmResponse);
|
||||
|
||||
let conversationStatus = "success";
|
||||
for await (const step of stream) {
|
||||
if (step.type === "STEP") {
|
||||
const stepDetails = JSON.parse(step.message as string);
|
||||
|
||||
if (stepDetails.skillStatus === ActionStatusEnum.TOOL_REQUEST) {
|
||||
conversationStatus = "need_approval";
|
||||
}
|
||||
|
||||
if (stepDetails.skillStatus === ActionStatusEnum.QUESTION) {
|
||||
conversationStatus = "need_attention";
|
||||
}
|
||||
|
||||
await updateExecutionStep(
|
||||
{ ...stepDetails },
|
||||
agentConversationHistory.id,
|
||||
);
|
||||
|
||||
agentUserMessage += stepDetails.userMessage;
|
||||
|
||||
await updateConversationHistoryMessage(
|
||||
agentUserMessage,
|
||||
agentConversationHistory.id,
|
||||
);
|
||||
} else if (step.type === "STREAM_END") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await updateConversationStatus(
|
||||
conversationStatus,
|
||||
payload.conversationId,
|
||||
);
|
||||
|
||||
// Deduct credits for chat message
|
||||
if (init?.conversation.workspaceId) {
|
||||
await deductCredits(init.conversation.workspaceId, "chatMessage");
|
||||
}
|
||||
|
||||
if (init?.tokenId) {
|
||||
await deletePersonalAccessToken(init.tokenId);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
await updateConversationStatus("failed", payload.conversationId);
|
||||
if (init?.tokenId) {
|
||||
await deletePersonalAccessToken(init.tokenId);
|
||||
}
|
||||
throw new Error(e as string);
|
||||
}
|
||||
},
|
||||
});
|
||||
@ -1,159 +0,0 @@
|
||||
export const REACT_SYSTEM_PROMPT = `
|
||||
You are a helpful AI assistant with access to user memory. Your primary capabilities are:
|
||||
|
||||
1. **Memory-First Approach**: Always check user memory first to understand context and previous interactions
|
||||
2. **Intelligent Information Gathering**: Analyze queries to determine if current information is needed
|
||||
3. **Memory Management**: Help users store, retrieve, and organize information in their memory
|
||||
4. **Contextual Assistance**: Use memory to provide personalized and contextual responses
|
||||
|
||||
<context>
|
||||
{{CONTEXT}}
|
||||
</context>
|
||||
|
||||
<information_gathering>
|
||||
Follow this intelligent approach for information gathering:
|
||||
|
||||
1. **MEMORY FIRST** (Always Required)
|
||||
- Always check memory FIRST using core--search_memory before any other actions
|
||||
- Consider this your highest priority for EVERY interaction - as essential as breathing
|
||||
- Memory provides context, personal preferences, and historical information
|
||||
- Use memory to understand user's background, ongoing projects, and past conversations
|
||||
|
||||
2. **INFORMATION SYNTHESIS** (Combine Sources)
|
||||
- Use memory to personalize current information based on user preferences
|
||||
- Always store new useful information in memory using core--add_memory
|
||||
|
||||
3. **TRAINING KNOWLEDGE** (Foundation)
|
||||
- Use your training knowledge as the foundation for analysis and explanation
|
||||
- Apply training knowledge to interpret and contextualize information from memory
|
||||
- Indicate when you're using training knowledge vs. live information sources
|
||||
|
||||
EXECUTION APPROACH:
|
||||
- Memory search is mandatory for every interaction
|
||||
- Always indicate your information sources in responses
|
||||
</information_gathering>
|
||||
|
||||
<memory>
|
||||
QUERY FORMATION:
|
||||
- Write specific factual statements as queries (e.g., "user email address" not "what is the user's email?")
|
||||
- Create multiple targeted memory queries for complex requests
|
||||
|
||||
KEY QUERY AREAS:
|
||||
- Personal context: user name, location, identity, work context
|
||||
- Project context: repositories, codebases, current work, team members
|
||||
- Task context: recent tasks, ongoing projects, deadlines, priorities
|
||||
- Integration context: GitHub repos, Slack channels, Linear projects, connected services
|
||||
- Communication patterns: email preferences, notification settings, workflow automation
|
||||
- Technical context: coding languages, frameworks, development environment
|
||||
- Collaboration context: team members, project stakeholders, meeting patterns
|
||||
- Preferences: likes, dislikes, communication style, tool preferences
|
||||
- History: previous discussions, past requests, completed work, recurring issues
|
||||
- Automation rules: user-defined workflows, triggers, automation preferences
|
||||
|
||||
MEMORY USAGE:
|
||||
- Execute multiple memory queries in parallel rather than sequentially
|
||||
- Batch related memory queries when possible
|
||||
- Prioritize recent information over older memories
|
||||
- Create comprehensive context-aware queries based on user message/activity content
|
||||
- Extract and query SEMANTIC CONTENT, not just structural metadata
|
||||
- Parse titles, descriptions, and content for actual subject matter keywords
|
||||
- Search internal SOL tasks/conversations that may relate to the same topics
|
||||
- Query ALL relatable concepts, not just direct keywords or IDs
|
||||
- Search for similar past situations, patterns, and related work
|
||||
- Include synonyms, related terms, and contextual concepts in queries
|
||||
- Query user's historical approach to similar requests or activities
|
||||
- Search for connected projects, tasks, conversations, and collaborations
|
||||
- Retrieve workflow patterns and past decision-making context
|
||||
- Query broader domain context beyond immediate request scope
|
||||
- Remember: SOL tracks work that external tools don't - search internal content thoroughly
|
||||
- Blend memory insights naturally into responses
|
||||
- Verify you've checked relevant memory before finalizing ANY response
|
||||
|
||||
</memory>
|
||||
|
||||
<external_services>
|
||||
- To use: load_mcp with EXACT integration name from the available list
|
||||
- Can load multiple at once with an array
|
||||
- Only load when tools are NOT already available in your current toolset
|
||||
- If a tool is already available, use it directly without load_mcp
|
||||
- If requested integration unavailable: inform user politely
|
||||
</external_services>
|
||||
|
||||
<tool_calling>
|
||||
You have tools at your disposal to assist users:
|
||||
|
||||
CORE PRINCIPLES:
|
||||
- Use tools only when necessary for the task at hand
|
||||
- Always check memory FIRST before making other tool calls
|
||||
- Execute multiple operations in parallel whenever possible
|
||||
- Use sequential calls only when output of one is required for input of another
|
||||
|
||||
PARAMETER HANDLING:
|
||||
- Follow tool schemas exactly with all required parameters
|
||||
- Only use values that are:
|
||||
• Explicitly provided by the user (use EXACTLY as given)
|
||||
• Reasonably inferred from context
|
||||
• Retrieved from memory or prior tool calls
|
||||
- Never make up values for required parameters
|
||||
- Omit optional parameters unless clearly needed
|
||||
- Analyze user's descriptive terms for parameter clues
|
||||
|
||||
TOOL SELECTION:
|
||||
- Never call tools not provided in this conversation
|
||||
- Skip tool calls for general questions you can answer directly from memory/knowledge
|
||||
- For identical operations on multiple items, use parallel tool calls
|
||||
- Default to parallel execution (3-5× faster than sequential calls)
|
||||
- You can always access external service tools by loading them with load_mcp first
|
||||
|
||||
TOOL MENTION HANDLING:
|
||||
When user message contains <mention data-id="tool_name" data-label="tool"></mention>:
|
||||
- Extract tool_name from data-id attribute
|
||||
- First check if it's a built-in tool; if not, check EXTERNAL SERVICES TOOLS
|
||||
- If available: Load it with load_mcp and focus on addressing the request with this tool
|
||||
- If unavailable: Inform user and suggest alternatives if possible
|
||||
- For multiple tool mentions: Load all applicable tools in a single load_mcp call
|
||||
|
||||
ERROR HANDLING:
|
||||
- If a tool returns an error, try fixing parameters before retrying
|
||||
- If you can't resolve an error, explain the issue to the user
|
||||
- Consider alternative tools when primary tools are unavailable
|
||||
</tool_calling>
|
||||
|
||||
<communication>
|
||||
Use EXACTLY ONE of these formats for all user-facing communication:
|
||||
|
||||
PROGRESS UPDATES - During processing:
|
||||
- Use the core--progress_update tool to keep users informed
|
||||
- Update users about what you're discovering or doing next
|
||||
- Keep messages clear and user-friendly
|
||||
- Avoid technical jargon
|
||||
|
||||
QUESTIONS - When you need information:
|
||||
<question_response>
|
||||
<p>[Your question with HTML formatting]</p>
|
||||
</question_response>
|
||||
|
||||
- Ask questions only when you cannot find information through memory, or tools
|
||||
- Be specific about what you need to know
|
||||
- Provide context for why you're asking
|
||||
|
||||
FINAL ANSWERS - When completing tasks:
|
||||
<final_response>
|
||||
<p>[Your answer with HTML formatting]</p>
|
||||
</final_response>
|
||||
|
||||
CRITICAL:
|
||||
- Use ONE format per turn
|
||||
- Apply proper HTML formatting (<h1>, <h2>, <p>, <ul>, <li>, etc.)
|
||||
- Never mix communication formats
|
||||
- Keep responses clear and helpful
|
||||
- Always indicate your information sources (memory, and/or knowledge)
|
||||
</communication>
|
||||
`;
|
||||
|
||||
export const REACT_USER_PROMPT = `
|
||||
Here is the user message:
|
||||
<user_message>
|
||||
{{USER_MESSAGE}}
|
||||
</user_message>
|
||||
`;
|
||||
@ -1,294 +0,0 @@
|
||||
import fs from "fs";
|
||||
import path from "node:path";
|
||||
|
||||
import { anthropic } from "@ai-sdk/anthropic";
|
||||
import { google } from "@ai-sdk/google";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
import { logger } from "@trigger.dev/sdk/v3";
|
||||
import {
|
||||
type CoreMessage,
|
||||
type LanguageModelV1,
|
||||
streamText,
|
||||
type ToolSet,
|
||||
} from "ai";
|
||||
import { createOllama } from "ollama-ai-provider";
|
||||
|
||||
import { type AgentMessageType, Message } from "./types";
|
||||
|
||||
interface State {
|
||||
inTag: boolean;
|
||||
messageEnded: boolean;
|
||||
message: string;
|
||||
lastSent: string;
|
||||
}
|
||||
|
||||
export interface ExecutionState {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
agentFlow: any;
|
||||
userMessage: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export async function* processTag(
|
||||
state: State,
|
||||
totalMessage: string,
|
||||
chunk: string,
|
||||
startTag: string,
|
||||
endTag: string,
|
||||
states: { start: string; chunk: string; end: string },
|
||||
extraParams: Record<string, string> = {},
|
||||
) {
|
||||
let comingFromStart = false;
|
||||
|
||||
if (!state.messageEnded) {
|
||||
if (!state.inTag) {
|
||||
const startIndex = totalMessage.indexOf(startTag);
|
||||
if (startIndex !== -1) {
|
||||
state.inTag = true;
|
||||
// Send MESSAGE_START when we first enter the tag
|
||||
yield Message("", states.start as AgentMessageType, extraParams);
|
||||
const chunkToSend = totalMessage.slice(startIndex + startTag.length);
|
||||
state.message += chunkToSend;
|
||||
comingFromStart = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (state.inTag) {
|
||||
// Check if chunk contains end tag
|
||||
const hasEndTag = chunk.includes(endTag);
|
||||
const hasStartTag = chunk.includes(startTag);
|
||||
const hasClosingTag = chunk.includes("</");
|
||||
|
||||
// Check if we're currently accumulating a potential end tag
|
||||
const accumulatingEndTag = state.message.endsWith("</") ||
|
||||
state.message.match(/<\/[a-z_]*$/i);
|
||||
|
||||
if (hasClosingTag && !hasStartTag && !hasEndTag) {
|
||||
// If chunk only has </ but not the full end tag, accumulate it
|
||||
state.message += chunk;
|
||||
} else if (accumulatingEndTag) {
|
||||
// Continue accumulating if we're in the middle of a potential end tag
|
||||
state.message += chunk;
|
||||
// Check if we now have the complete end tag
|
||||
if (state.message.includes(endTag)) {
|
||||
// Process the complete message with end tag
|
||||
const endIndex = state.message.indexOf(endTag);
|
||||
const finalMessage = state.message.slice(0, endIndex).trim();
|
||||
const messageToSend = finalMessage.slice(
|
||||
finalMessage.indexOf(state.lastSent) + state.lastSent.length,
|
||||
);
|
||||
|
||||
if (messageToSend) {
|
||||
yield Message(
|
||||
messageToSend,
|
||||
states.chunk as AgentMessageType,
|
||||
extraParams,
|
||||
);
|
||||
}
|
||||
yield Message("", states.end as AgentMessageType, extraParams);
|
||||
|
||||
state.message = finalMessage;
|
||||
state.messageEnded = true;
|
||||
}
|
||||
} else if (hasEndTag || (!hasEndTag && !hasClosingTag)) {
|
||||
let currentMessage = comingFromStart
|
||||
? state.message
|
||||
: state.message + chunk;
|
||||
|
||||
const endIndex = currentMessage.indexOf(endTag);
|
||||
|
||||
if (endIndex !== -1) {
|
||||
// For the final chunk before the end tag
|
||||
currentMessage = currentMessage.slice(0, endIndex).trim();
|
||||
const messageToSend = currentMessage.slice(
|
||||
currentMessage.indexOf(state.lastSent) + state.lastSent.length,
|
||||
);
|
||||
|
||||
if (messageToSend) {
|
||||
yield Message(
|
||||
messageToSend,
|
||||
states.chunk as AgentMessageType,
|
||||
extraParams,
|
||||
);
|
||||
}
|
||||
// Send MESSAGE_END when we reach the end tag
|
||||
yield Message("", states.end as AgentMessageType, extraParams);
|
||||
|
||||
state.message = currentMessage;
|
||||
state.messageEnded = true;
|
||||
} else {
|
||||
const diff = currentMessage.slice(
|
||||
currentMessage.indexOf(state.lastSent) + state.lastSent.length,
|
||||
);
|
||||
|
||||
// For chunks in between start and end
|
||||
const messageToSend = comingFromStart ? state.message : diff;
|
||||
|
||||
if (messageToSend) {
|
||||
state.lastSent = messageToSend;
|
||||
yield Message(
|
||||
messageToSend,
|
||||
states.chunk as AgentMessageType,
|
||||
extraParams,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
state.message = currentMessage;
|
||||
state.lastSent = state.message;
|
||||
} else {
|
||||
state.message += chunk;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function* generate(
|
||||
messages: CoreMessage[],
|
||||
isProgressUpdate: boolean = false,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
onFinish?: (event: any) => void,
|
||||
tools?: ToolSet,
|
||||
system?: string,
|
||||
model?: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
): AsyncGenerator<
|
||||
| string
|
||||
| {
|
||||
type: string;
|
||||
toolName: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
args?: any;
|
||||
toolCallId?: string;
|
||||
message?: string;
|
||||
}
|
||||
> {
|
||||
// Check for API keys
|
||||
const anthropicKey = process.env.ANTHROPIC_API_KEY;
|
||||
const googleKey = process.env.GOOGLE_GENERATIVE_AI_API_KEY;
|
||||
const openaiKey = process.env.OPENAI_API_KEY;
|
||||
let ollamaUrl = process.env.OLLAMA_URL;
|
||||
model = model || process.env.MODEL;
|
||||
|
||||
let modelInstance;
|
||||
let modelTemperature = Number(process.env.MODEL_TEMPERATURE) || 1;
|
||||
ollamaUrl = undefined;
|
||||
|
||||
// First check if Ollama URL exists and use Ollama
|
||||
if (ollamaUrl) {
|
||||
const ollama = createOllama({
|
||||
baseURL: ollamaUrl,
|
||||
});
|
||||
modelInstance = ollama(model || "llama2"); // Default to llama2 if no model specified
|
||||
} else {
|
||||
// If no Ollama, check other models
|
||||
switch (model) {
|
||||
case "claude-3-7-sonnet-20250219":
|
||||
case "claude-3-opus-20240229":
|
||||
case "claude-3-5-haiku-20241022":
|
||||
if (!anthropicKey) {
|
||||
throw new Error("No Anthropic API key found. Set ANTHROPIC_API_KEY");
|
||||
}
|
||||
modelInstance = anthropic(model);
|
||||
modelTemperature = 0.5;
|
||||
break;
|
||||
|
||||
case "gemini-2.5-flash-preview-04-17":
|
||||
case "gemini-2.5-pro-preview-03-25":
|
||||
case "gemini-2.0-flash":
|
||||
case "gemini-2.0-flash-lite":
|
||||
if (!googleKey) {
|
||||
throw new Error("No Google API key found. Set GOOGLE_API_KEY");
|
||||
}
|
||||
modelInstance = google(model);
|
||||
break;
|
||||
|
||||
case "gpt-4.1-2025-04-14":
|
||||
case "gpt-4.1-mini-2025-04-14":
|
||||
case "gpt-5-mini-2025-08-07":
|
||||
case "gpt-5-2025-08-07":
|
||||
case "gpt-4.1-nano-2025-04-14":
|
||||
if (!openaiKey) {
|
||||
throw new Error("No OpenAI API key found. Set OPENAI_API_KEY");
|
||||
}
|
||||
modelInstance = openai(model);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("starting stream");
|
||||
// Try Anthropic next if key exists
|
||||
if (modelInstance) {
|
||||
try {
|
||||
const { textStream, fullStream } = streamText({
|
||||
model: modelInstance as LanguageModelV1,
|
||||
messages,
|
||||
temperature: modelTemperature,
|
||||
maxSteps: 10,
|
||||
tools,
|
||||
...(isProgressUpdate
|
||||
? { toolChoice: { type: "tool", toolName: "core--progress_update" } }
|
||||
: {}),
|
||||
toolCallStreaming: true,
|
||||
onFinish,
|
||||
...(system ? { system } : {}),
|
||||
});
|
||||
|
||||
for await (const chunk of textStream) {
|
||||
yield chunk;
|
||||
}
|
||||
|
||||
for await (const fullChunk of fullStream) {
|
||||
if (fullChunk.type === "tool-call") {
|
||||
yield {
|
||||
type: "tool-call",
|
||||
toolName: fullChunk.toolName,
|
||||
toolCallId: fullChunk.toolCallId,
|
||||
args: fullChunk.args,
|
||||
};
|
||||
}
|
||||
|
||||
if (fullChunk.type === "error") {
|
||||
// Log the error to a file
|
||||
const errorLogsDir = path.join(__dirname, "../../../../logs/errors");
|
||||
|
||||
// Ensure the directory exists
|
||||
try {
|
||||
if (!fs.existsSync(errorLogsDir)) {
|
||||
fs.mkdirSync(errorLogsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Create a timestamped error log file
|
||||
const timestamp = new Date().toISOString().replace(/:/g, "-");
|
||||
const errorLogPath = path.join(
|
||||
errorLogsDir,
|
||||
`llm-error-${timestamp}.json`,
|
||||
);
|
||||
|
||||
// Write the error to the file
|
||||
fs.writeFileSync(
|
||||
errorLogPath,
|
||||
JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
error: fullChunk.error,
|
||||
}),
|
||||
);
|
||||
|
||||
logger.error(`LLM error logged to ${errorLogPath}`);
|
||||
} catch (err) {
|
||||
logger.error(`Failed to log LLM error: ${err}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
logger.error(e as string);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("No valid LLM configuration found");
|
||||
}
|
||||
@ -1,46 +0,0 @@
|
||||
export interface AgentStep {
|
||||
agent: string;
|
||||
goal: string;
|
||||
reasoning: string;
|
||||
}
|
||||
|
||||
export enum AgentMessageType {
|
||||
STREAM_START = 'STREAM_START',
|
||||
STREAM_END = 'STREAM_END',
|
||||
|
||||
// Used in ReACT based prompting
|
||||
THOUGHT_START = 'THOUGHT_START',
|
||||
THOUGHT_CHUNK = 'THOUGHT_CHUNK',
|
||||
THOUGHT_END = 'THOUGHT_END',
|
||||
|
||||
// Message types
|
||||
MESSAGE_START = 'MESSAGE_START',
|
||||
MESSAGE_CHUNK = 'MESSAGE_CHUNK',
|
||||
MESSAGE_END = 'MESSAGE_END',
|
||||
|
||||
// This is used to return action input
|
||||
SKILL_START = 'SKILL_START',
|
||||
SKILL_CHUNK = 'SKILL_CHUNK',
|
||||
SKILL_END = 'SKILL_END',
|
||||
|
||||
STEP = 'STEP',
|
||||
ERROR = 'ERROR',
|
||||
}
|
||||
|
||||
export interface AgentMessage {
|
||||
message?: string;
|
||||
type: AgentMessageType;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
metadata: Record<string, any>;
|
||||
}
|
||||
|
||||
export const Message = (
|
||||
message: string,
|
||||
type: AgentMessageType,
|
||||
extraParams: Record<string, string> = {},
|
||||
): AgentMessage => {
|
||||
// For all message types, we use the message field
|
||||
// The type field differentiates how the message should be interpreted
|
||||
// For STEP and SKILL types, the message can contain JSON data as a string
|
||||
return { message, type, metadata: extraParams };
|
||||
};
|
||||
@ -1,115 +0,0 @@
|
||||
import { queue, task } from "@trigger.dev/sdk";
|
||||
import { z } from "zod";
|
||||
import { ClusteringService } from "~/services/clustering.server";
|
||||
import { logger } from "~/services/logger.service";
|
||||
|
||||
const clusteringService = new ClusteringService();
|
||||
|
||||
// Define the payload schema for cluster tasks
|
||||
export const ClusterPayload = z.object({
|
||||
userId: z.string(),
|
||||
mode: z.enum(["auto", "incremental", "complete", "drift"]).default("auto"),
|
||||
forceComplete: z.boolean().default(false),
|
||||
});
|
||||
|
||||
const clusterQueue = queue({
|
||||
name: "cluster-queue",
|
||||
concurrencyLimit: 10,
|
||||
});
|
||||
|
||||
/**
|
||||
* Single clustering task that handles all clustering operations based on payload mode
|
||||
*/
|
||||
export const clusterTask = task({
|
||||
id: "cluster",
|
||||
queue: clusterQueue,
|
||||
maxDuration: 1800, // 30 minutes max
|
||||
run: async (payload: z.infer<typeof ClusterPayload>) => {
|
||||
logger.info(`Starting ${payload.mode} clustering task for user ${payload.userId}`);
|
||||
|
||||
try {
|
||||
let result;
|
||||
|
||||
switch (payload.mode) {
|
||||
case "incremental":
|
||||
result = await clusteringService.performIncrementalClustering(
|
||||
payload.userId,
|
||||
);
|
||||
logger.info(`Incremental clustering completed for user ${payload.userId}:`, {
|
||||
newStatementsProcessed: result.newStatementsProcessed,
|
||||
newClustersCreated: result.newClustersCreated,
|
||||
});
|
||||
break;
|
||||
|
||||
case "complete":
|
||||
result = await clusteringService.performCompleteClustering(
|
||||
payload.userId,
|
||||
);
|
||||
logger.info(`Complete clustering completed for user ${payload.userId}:`, {
|
||||
clustersCreated: result.clustersCreated,
|
||||
statementsProcessed: result.statementsProcessed,
|
||||
});
|
||||
break;
|
||||
|
||||
case "drift":
|
||||
// First detect drift
|
||||
const driftMetrics = await clusteringService.detectClusterDrift(
|
||||
payload.userId,
|
||||
);
|
||||
|
||||
if (driftMetrics.driftDetected) {
|
||||
// Handle drift by splitting low-cohesion clusters
|
||||
const driftResult = await clusteringService.handleClusterDrift(
|
||||
payload.userId,
|
||||
);
|
||||
|
||||
logger.info(`Cluster drift handling completed for user ${payload.userId}:`, {
|
||||
driftDetected: true,
|
||||
clustersProcessed: driftResult.clustersProcessed,
|
||||
newClustersCreated: driftResult.newClustersCreated,
|
||||
splitClusters: driftResult.splitClusters,
|
||||
});
|
||||
|
||||
result = {
|
||||
driftDetected: true,
|
||||
...driftResult,
|
||||
driftMetrics,
|
||||
};
|
||||
} else {
|
||||
logger.info(`No cluster drift detected for user ${payload.userId}`);
|
||||
result = {
|
||||
driftDetected: false,
|
||||
clustersProcessed: 0,
|
||||
newClustersCreated: 0,
|
||||
splitClusters: [],
|
||||
driftMetrics,
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
case "auto":
|
||||
default:
|
||||
result = await clusteringService.performClustering(
|
||||
payload.userId,
|
||||
payload.forceComplete,
|
||||
);
|
||||
logger.info(`Auto clustering completed for user ${payload.userId}:`, {
|
||||
clustersCreated: result.clustersCreated,
|
||||
statementsProcessed: result.statementsProcessed,
|
||||
approach: result.approach,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`${payload.mode} clustering failed for user ${payload.userId}:`, {
|
||||
error,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
});
|
||||
@ -1,292 +0,0 @@
|
||||
import { type CoreMessage } from "ai";
|
||||
import { logger } from "@trigger.dev/sdk/v3";
|
||||
import { generate } from "./stream-utils";
|
||||
import { processTag } from "../chat/stream-utils";
|
||||
import { type AgentMessage, AgentMessageType, Message } from "../chat/types";
|
||||
import { type TotalCost } from "../utils/types";
|
||||
|
||||
/**
|
||||
* Run the deep search ReAct loop
|
||||
* Async generator that yields AgentMessage objects for streaming
|
||||
* Follows the exact same pattern as chat-utils.ts
|
||||
*/
|
||||
export async function* run(
|
||||
initialMessages: CoreMessage[],
|
||||
searchTool: any,
|
||||
): AsyncGenerator<AgentMessage, any, any> {
|
||||
let messages = [...initialMessages];
|
||||
let completed = false;
|
||||
let guardLoop = 0;
|
||||
let searchCount = 0;
|
||||
let totalEpisodesFound = 0;
|
||||
const seenEpisodeIds = new Set<string>(); // Track unique episodes
|
||||
const totalCost: TotalCost = {
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
cost: 0,
|
||||
};
|
||||
|
||||
const tools = {
|
||||
searchMemory: searchTool,
|
||||
};
|
||||
|
||||
logger.info("Starting deep search ReAct loop");
|
||||
|
||||
try {
|
||||
while (!completed && guardLoop < 50) {
|
||||
logger.info(
|
||||
`ReAct loop iteration ${guardLoop}, searches: ${searchCount}`,
|
||||
);
|
||||
|
||||
// Call LLM with current message history
|
||||
const response = generate(
|
||||
messages,
|
||||
(event) => {
|
||||
const usage = event.usage;
|
||||
totalCost.inputTokens += usage.promptTokens;
|
||||
totalCost.outputTokens += usage.completionTokens;
|
||||
},
|
||||
tools,
|
||||
);
|
||||
|
||||
let totalMessage = "";
|
||||
const toolCalls: any[] = [];
|
||||
|
||||
// States for streaming final_response tags
|
||||
const messageState = {
|
||||
inTag: false,
|
||||
message: "",
|
||||
messageEnded: false,
|
||||
lastSent: "",
|
||||
};
|
||||
|
||||
// Process streaming response
|
||||
for await (const chunk of response) {
|
||||
if (typeof chunk === "object" && chunk.type === "tool-call") {
|
||||
// Agent made a tool call
|
||||
toolCalls.push(chunk);
|
||||
logger.info(`Tool call: ${chunk.toolName}`);
|
||||
} else if (typeof chunk === "string") {
|
||||
totalMessage += chunk;
|
||||
|
||||
// Stream final_response tags using processTag
|
||||
if (!messageState.messageEnded) {
|
||||
yield* processTag(
|
||||
messageState,
|
||||
totalMessage,
|
||||
chunk,
|
||||
"<final_response>",
|
||||
"</final_response>",
|
||||
{
|
||||
start: AgentMessageType.MESSAGE_START,
|
||||
chunk: AgentMessageType.MESSAGE_CHUNK,
|
||||
end: AgentMessageType.MESSAGE_END,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for final response
|
||||
if (totalMessage.includes("<final_response>")) {
|
||||
const match = totalMessage.match(
|
||||
/<final_response>(.*?)<\/final_response>/s,
|
||||
);
|
||||
|
||||
if (match) {
|
||||
// Accept synthesis - completed
|
||||
completed = true;
|
||||
logger.info(
|
||||
`Final synthesis accepted after ${searchCount} searches, ${totalEpisodesFound} unique episodes found`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Execute tool calls in parallel for better performance
|
||||
if (toolCalls.length > 0) {
|
||||
// Notify about all searches starting
|
||||
for (const toolCall of toolCalls) {
|
||||
logger.info(`Executing search: ${JSON.stringify(toolCall.args)}`);
|
||||
yield Message("", AgentMessageType.SKILL_START);
|
||||
yield Message(
|
||||
`\nSearching memory: "${toolCall.args.query}"...\n`,
|
||||
AgentMessageType.SKILL_CHUNK,
|
||||
);
|
||||
yield Message("", AgentMessageType.SKILL_END);
|
||||
}
|
||||
|
||||
// Execute all searches in parallel
|
||||
const searchPromises = toolCalls.map((toolCall) =>
|
||||
searchTool.execute(toolCall.args).then((result: any) => ({
|
||||
toolCall,
|
||||
result,
|
||||
})),
|
||||
);
|
||||
|
||||
const searchResults = await Promise.all(searchPromises);
|
||||
|
||||
// Process results and add to message history
|
||||
for (const { toolCall, result } of searchResults) {
|
||||
searchCount++;
|
||||
|
||||
// Deduplicate episodes - track unique IDs
|
||||
let uniqueNewEpisodes = 0;
|
||||
if (result.episodes && Array.isArray(result.episodes)) {
|
||||
for (const episode of result.episodes) {
|
||||
const episodeId =
|
||||
episode.id || episode._id || JSON.stringify(episode);
|
||||
if (!seenEpisodeIds.has(episodeId)) {
|
||||
seenEpisodeIds.add(episodeId);
|
||||
uniqueNewEpisodes++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const episodesInThisSearch = result.episodes?.length || 0;
|
||||
totalEpisodesFound = seenEpisodeIds.size; // Use unique count
|
||||
|
||||
messages.push({
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool-call",
|
||||
toolCallId: toolCall.toolCallId,
|
||||
toolName: toolCall.toolName,
|
||||
args: toolCall.args,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Add tool result to message history
|
||||
messages.push({
|
||||
role: "tool",
|
||||
content: [
|
||||
{
|
||||
type: "tool-result",
|
||||
toolName: toolCall.toolName,
|
||||
toolCallId: toolCall.toolCallId,
|
||||
result: result,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`Search ${searchCount} completed: ${episodesInThisSearch} episodes (${uniqueNewEpisodes} new, ${totalEpisodesFound} unique total)`,
|
||||
);
|
||||
}
|
||||
|
||||
// If found no episodes and haven't exhausted search attempts, require more searches
|
||||
if (totalEpisodesFound === 0 && searchCount < 7) {
|
||||
logger.info(
|
||||
`Agent attempted synthesis with 0 unique episodes after ${searchCount} searches - requiring more attempts`,
|
||||
);
|
||||
|
||||
yield Message("", AgentMessageType.SKILL_START);
|
||||
yield Message(
|
||||
`No relevant context found yet - trying different search angles...`,
|
||||
AgentMessageType.SKILL_CHUNK,
|
||||
);
|
||||
yield Message("", AgentMessageType.SKILL_END);
|
||||
|
||||
messages.push({
|
||||
role: "system",
|
||||
content: `You have performed ${searchCount} searches but found 0 unique relevant episodes. Your queries may be too abstract or not matching the user's actual conversation topics.
|
||||
|
||||
Review your DECOMPOSITION:
|
||||
- Are you using specific terms from the content?
|
||||
- Try searching broader related topics the user might have discussed
|
||||
- Try different terminology or related concepts
|
||||
- Search for user's projects, work areas, or interests
|
||||
|
||||
Continue with different search strategies (you can search up to 7-10 times total).`,
|
||||
});
|
||||
|
||||
guardLoop++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Soft nudging after all searches executed (awareness, not commands)
|
||||
if (totalEpisodesFound >= 30 && searchCount >= 3) {
|
||||
logger.info(
|
||||
`Nudging: ${totalEpisodesFound} unique episodes found - suggesting synthesis consideration`,
|
||||
);
|
||||
|
||||
messages.push({
|
||||
role: "system",
|
||||
content: `Context awareness: You have found ${totalEpisodesFound} unique episodes across ${searchCount} searches. This represents substantial context. Consider whether you have sufficient information for quality synthesis, or if additional search angles would meaningfully improve understanding.`,
|
||||
});
|
||||
} else if (totalEpisodesFound >= 15 && searchCount >= 5) {
|
||||
logger.info(
|
||||
`Nudging: ${totalEpisodesFound} unique episodes after ${searchCount} searches - suggesting evaluation`,
|
||||
);
|
||||
|
||||
messages.push({
|
||||
role: "system",
|
||||
content: `Progress update: You have ${totalEpisodesFound} unique episodes from ${searchCount} searches. Evaluate whether you have covered the main angles from your decomposition, or if important aspects remain unexplored.`,
|
||||
});
|
||||
} else if (searchCount >= 7) {
|
||||
logger.info(
|
||||
`Nudging: ${searchCount} searches completed with ${totalEpisodesFound} unique episodes`,
|
||||
);
|
||||
|
||||
messages.push({
|
||||
role: "system",
|
||||
content: `Search depth: You have performed ${searchCount} searches and found ${totalEpisodesFound} unique episodes. Consider whether additional searches would yield meaningfully different context, or if it's time to synthesize what you've discovered.`,
|
||||
});
|
||||
}
|
||||
if (searchCount >= 10) {
|
||||
logger.info(
|
||||
`Reached maximum search limit (10), forcing synthesis with ${totalEpisodesFound} unique episodes`,
|
||||
);
|
||||
|
||||
yield Message("", AgentMessageType.SKILL_START);
|
||||
yield Message(
|
||||
`Maximum searches reached - synthesizing results...`,
|
||||
AgentMessageType.SKILL_CHUNK,
|
||||
);
|
||||
yield Message("", AgentMessageType.SKILL_END);
|
||||
|
||||
messages.push({
|
||||
role: "system",
|
||||
content: `You have performed 10 searches and found ${totalEpisodesFound} unique episodes. This is the maximum allowed. You MUST now provide your final synthesis wrapped in <final_response> tags based on what you've found.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Safety check - if no tool calls and no final response, something went wrong
|
||||
if (
|
||||
toolCalls.length === 0 &&
|
||||
!totalMessage.includes("<final_response>")
|
||||
) {
|
||||
logger.warn("Agent produced neither tool calls nor final response");
|
||||
|
||||
messages.push({
|
||||
role: "system",
|
||||
content:
|
||||
"You must either use the searchMemory tool to search for more context, or provide your final synthesis wrapped in <final_response> tags.",
|
||||
});
|
||||
}
|
||||
|
||||
guardLoop++;
|
||||
}
|
||||
|
||||
if (!completed) {
|
||||
logger.warn(
|
||||
`Loop ended without completion after ${guardLoop} iterations`,
|
||||
);
|
||||
yield Message("", AgentMessageType.MESSAGE_START);
|
||||
yield Message(
|
||||
"Deep search did not complete - maximum iterations reached.",
|
||||
AgentMessageType.MESSAGE_CHUNK,
|
||||
);
|
||||
yield Message("", AgentMessageType.MESSAGE_END);
|
||||
}
|
||||
|
||||
yield Message("Stream ended", AgentMessageType.STREAM_END);
|
||||
} catch (error) {
|
||||
logger.error(`Deep search error: ${error}`);
|
||||
yield Message((error as Error).message, AgentMessageType.ERROR);
|
||||
yield Message("Stream ended", AgentMessageType.STREAM_END);
|
||||
}
|
||||
}
|
||||
@ -1,85 +0,0 @@
|
||||
import { metadata, task } from "@trigger.dev/sdk";
|
||||
import { type CoreMessage } from "ai";
|
||||
import { logger } from "@trigger.dev/sdk/v3";
|
||||
import { nanoid } from "nanoid";
|
||||
import {
|
||||
deletePersonalAccessToken,
|
||||
getOrCreatePersonalAccessToken,
|
||||
} from "../utils/utils";
|
||||
import { getReActPrompt } from "./prompt";
|
||||
import { type DeepSearchPayload, type DeepSearchResponse } from "./types";
|
||||
import { createSearchMemoryTool } from "./utils";
|
||||
import { run } from "./deep-search-utils";
|
||||
import { AgentMessageType } from "../chat/types";
|
||||
|
||||
export const deepSearch = task({
|
||||
id: "deep-search",
|
||||
maxDuration: 3000,
|
||||
run: async (payload: DeepSearchPayload): Promise<DeepSearchResponse> => {
|
||||
const { content, userId, stream, metadata: meta, intentOverride } = payload;
|
||||
|
||||
const randomKeyName = `deepSearch_${nanoid(10)}`;
|
||||
|
||||
// Get or create token for search API calls
|
||||
const pat = await getOrCreatePersonalAccessToken({
|
||||
name: randomKeyName,
|
||||
userId: userId as string,
|
||||
});
|
||||
|
||||
if (!pat?.token) {
|
||||
throw new Error("Failed to create personal access token");
|
||||
}
|
||||
|
||||
try {
|
||||
// Create search tool that agent will use
|
||||
const searchTool = createSearchMemoryTool(pat.token);
|
||||
|
||||
// Build initial messages with ReAct prompt
|
||||
const initialMessages: CoreMessage[] = [
|
||||
{
|
||||
role: "system",
|
||||
content: getReActPrompt(meta, intentOverride),
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: `CONTENT TO ANALYZE:\n${content}\n\nPlease search my memory for relevant context and synthesize what you find.`,
|
||||
},
|
||||
];
|
||||
|
||||
// Run the ReAct loop generator
|
||||
const llmResponse = run(initialMessages, searchTool);
|
||||
|
||||
// Streaming mode: stream via metadata.stream like chat.ts does
|
||||
// This makes all message types available to clients in real-time
|
||||
const messageStream = await metadata.stream("messages", llmResponse);
|
||||
|
||||
let synthesis = "";
|
||||
|
||||
for await (const step of messageStream) {
|
||||
// MESSAGE_CHUNK: Final synthesis - accumulate and stream
|
||||
if (step.type === AgentMessageType.MESSAGE_CHUNK) {
|
||||
synthesis += step.message;
|
||||
}
|
||||
|
||||
// STREAM_END: Loop completed
|
||||
if (step.type === AgentMessageType.STREAM_END) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await deletePersonalAccessToken(pat?.id);
|
||||
|
||||
// Clean up any remaining tags
|
||||
synthesis = synthesis
|
||||
.replace(/<final_response>/gi, "")
|
||||
.replace(/<\/final_response>/gi, "")
|
||||
.trim();
|
||||
|
||||
return { synthesis };
|
||||
} catch (error) {
|
||||
await deletePersonalAccessToken(pat?.id);
|
||||
logger.error(`Deep search error: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
});
|
||||
@ -1,148 +0,0 @@
|
||||
export function getReActPrompt(
|
||||
metadata?: { source?: string; url?: string; pageTitle?: string },
|
||||
intentOverride?: string
|
||||
): string {
|
||||
const contextHints = [];
|
||||
|
||||
if (metadata?.source === "chrome" && metadata?.url?.includes("mail.google.com")) {
|
||||
contextHints.push("Content is from email - likely reading intent");
|
||||
}
|
||||
if (metadata?.source === "chrome" && metadata?.url?.includes("calendar.google.com")) {
|
||||
contextHints.push("Content is from calendar - likely meeting prep intent");
|
||||
}
|
||||
if (metadata?.source === "chrome" && metadata?.url?.includes("docs.google.com")) {
|
||||
contextHints.push("Content is from document editor - likely writing intent");
|
||||
}
|
||||
if (metadata?.source === "obsidian") {
|
||||
contextHints.push("Content is from note editor - likely writing or research intent");
|
||||
}
|
||||
|
||||
return `You are a memory research agent analyzing content to find relevant context.
|
||||
|
||||
YOUR PROCESS (ReAct Framework):
|
||||
|
||||
1. DECOMPOSE: First, break down the content into structured categories
|
||||
|
||||
Analyze the content and extract:
|
||||
a) ENTITIES: Specific people, project names, tools, products mentioned
|
||||
Example: "John Smith", "Phoenix API", "Redis", "mobile app"
|
||||
|
||||
b) TOPICS & CONCEPTS: Key subjects, themes, domains
|
||||
Example: "authentication", "database design", "performance optimization"
|
||||
|
||||
c) TEMPORAL MARKERS: Time references, deadlines, events
|
||||
Example: "last week's meeting", "Q2 launch", "yesterday's discussion"
|
||||
|
||||
d) ACTIONS & TASKS: What's being done, decided, or requested
|
||||
Example: "implement feature", "review code", "make decision on"
|
||||
|
||||
e) USER INTENT: What is the user trying to accomplish?
|
||||
${intentOverride ? `User specified: "${intentOverride}"` : "Infer from context: reading/writing/meeting prep/research/task tracking/review"}
|
||||
|
||||
2. FORM QUERIES: Create targeted search queries from your decomposition
|
||||
|
||||
Based on decomposition, form specific queries:
|
||||
- Search for each entity by name (people, projects, tools)
|
||||
- Search for topics the user has discussed before
|
||||
- Search for related work or conversations in this domain
|
||||
- Use the user's actual terminology, not generic concepts
|
||||
|
||||
EXAMPLE - Content: "Email from Sarah about the API redesign we discussed last week"
|
||||
Decomposition:
|
||||
- Entities: "Sarah", "API redesign"
|
||||
- Topics: "API design", "redesign"
|
||||
- Temporal: "last week"
|
||||
- Actions: "discussed", "email communication"
|
||||
- Intent: Reading (email) / meeting prep
|
||||
|
||||
Queries to form:
|
||||
✅ "Sarah" (find past conversations with Sarah)
|
||||
✅ "API redesign" or "API design" (find project discussions)
|
||||
✅ "last week" + "Sarah" (find recent context)
|
||||
✅ "meetings" or "discussions" (find related conversations)
|
||||
|
||||
❌ Avoid: "email communication patterns", "API architecture philosophy"
|
||||
(These are abstract - search what user actually discussed!)
|
||||
|
||||
3. SEARCH: Execute your queries using searchMemory tool
|
||||
- Start with 2-3 core searches based on main entities/topics
|
||||
- Make each search specific and targeted
|
||||
- Use actual terms from the content, not rephrased concepts
|
||||
|
||||
4. OBSERVE: Evaluate search results
|
||||
- Did you find relevant episodes? How many unique ones?
|
||||
- What specific context emerged?
|
||||
- What new entities/topics appeared in results?
|
||||
- Are there gaps in understanding?
|
||||
- Should you search more angles?
|
||||
|
||||
Note: Episode counts are automatically deduplicated across searches - overlapping episodes are only counted once.
|
||||
|
||||
5. REACT: Decide next action based on observations
|
||||
|
||||
STOPPING CRITERIA - Proceed to SYNTHESIZE if ANY of these are true:
|
||||
- You found 20+ unique episodes across your searches → ENOUGH CONTEXT
|
||||
- You performed 5+ searches and found relevant episodes → SUFFICIENT
|
||||
- You performed 7+ searches regardless of results → EXHAUSTED STRATEGIES
|
||||
- You found strong relevant context from multiple angles → COMPLETE
|
||||
|
||||
System nudges will provide awareness of your progress, but you decide when synthesis quality would be optimal.
|
||||
|
||||
If you found little/no context AND searched less than 7 times:
|
||||
- Try different query angles from your decomposition
|
||||
- Search broader related topics
|
||||
- Search user's projects or work areas
|
||||
- Try alternative terminology
|
||||
|
||||
⚠️ DO NOT search endlessly - if you found relevant episodes, STOP and synthesize!
|
||||
|
||||
6. SYNTHESIZE: After gathering sufficient context, provide final answer
|
||||
- Wrap your synthesis in <final_response> tags
|
||||
- Present direct factual context from memory - no meta-commentary
|
||||
- Write as if providing background context to an AI assistant
|
||||
- Include: facts, decisions, preferences, patterns, timelines
|
||||
- Note any gaps, contradictions, or evolution in thinking
|
||||
- Keep it concise and actionable
|
||||
- DO NOT use phrases like "Previous discussions on", "From conversations", "Past preferences indicate"
|
||||
- DO NOT use conversational language like "you said" or "you mentioned"
|
||||
- Present information as direct factual statements
|
||||
|
||||
FINAL RESPONSE FORMAT:
|
||||
<final_response>
|
||||
[Direct synthesized context - factual statements only]
|
||||
|
||||
Good examples:
|
||||
- "The API redesign focuses on performance and scalability. Key decisions: moving to GraphQL, caching layer with Redis."
|
||||
- "Project Phoenix launches Q2 2024. Main features: real-time sync, offline mode, collaborative editing."
|
||||
- "Sarah leads the backend team. Recent work includes authentication refactor and database migration."
|
||||
|
||||
Bad examples:
|
||||
❌ "Previous discussions on the API revealed..."
|
||||
❌ "From past conversations, it appears that..."
|
||||
❌ "Past preferences indicate..."
|
||||
❌ "The user mentioned that..."
|
||||
|
||||
Just state the facts directly.
|
||||
</final_response>
|
||||
|
||||
${contextHints.length > 0 ? `\nCONTEXT HINTS:\n${contextHints.join("\n")}` : ""}
|
||||
|
||||
CRITICAL REQUIREMENTS:
|
||||
- ALWAYS start with DECOMPOSE step - extract entities, topics, temporal markers, actions
|
||||
- Form specific queries from your decomposition - use user's actual terms
|
||||
- Minimum 3 searches required
|
||||
- Maximum 10 searches allowed - must synthesize after that
|
||||
- STOP and synthesize when you hit stopping criteria (20+ episodes, 5+ searches with results, 7+ searches total)
|
||||
- Each search should target different aspects from decomposition
|
||||
- Present synthesis directly without meta-commentary
|
||||
|
||||
SEARCH QUALITY CHECKLIST:
|
||||
✅ Queries use specific terms from content (names, projects, exact phrases)
|
||||
✅ Searched multiple angles from decomposition (entities, topics, related areas)
|
||||
✅ Stop when you have enough unique context - don't search endlessly
|
||||
✅ Tried alternative terminology if initial searches found nothing
|
||||
❌ Avoid generic/abstract queries that don't match user's vocabulary
|
||||
❌ Don't stop at 3 searches if you found zero unique episodes
|
||||
❌ Don't keep searching when you already found 20+ unique episodes
|
||||
}`
|
||||
}
|
||||
@ -1,68 +0,0 @@
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
import { logger } from "@trigger.dev/sdk/v3";
|
||||
import {
|
||||
type CoreMessage,
|
||||
type LanguageModelV1,
|
||||
streamText,
|
||||
type ToolSet,
|
||||
} from "ai";
|
||||
|
||||
/**
|
||||
* Generate LLM responses with tool calling support
|
||||
* Simplified version for deep-search use case - NO maxSteps for manual ReAct control
|
||||
*/
|
||||
export async function* generate(
|
||||
messages: CoreMessage[],
|
||||
onFinish?: (event: any) => void,
|
||||
tools?: ToolSet,
|
||||
model?: string,
|
||||
): AsyncGenerator<
|
||||
| string
|
||||
| {
|
||||
type: string;
|
||||
toolName: string;
|
||||
args?: any;
|
||||
toolCallId?: string;
|
||||
}
|
||||
> {
|
||||
const modelToUse = model || process.env.MODEL || "gpt-4.1-2025-04-14";
|
||||
const modelInstance = openai(modelToUse) as LanguageModelV1;
|
||||
|
||||
logger.info(`Starting LLM generation with model: ${modelToUse}`);
|
||||
|
||||
try {
|
||||
const { textStream, fullStream } = streamText({
|
||||
model: modelInstance,
|
||||
messages,
|
||||
temperature: 1,
|
||||
tools,
|
||||
// NO maxSteps - we handle tool execution manually in the ReAct loop
|
||||
toolCallStreaming: true,
|
||||
onFinish,
|
||||
});
|
||||
|
||||
// Yield text chunks
|
||||
for await (const chunk of textStream) {
|
||||
yield chunk;
|
||||
}
|
||||
|
||||
// Yield tool calls
|
||||
for await (const fullChunk of fullStream) {
|
||||
if (fullChunk.type === "tool-call") {
|
||||
yield {
|
||||
type: "tool-call",
|
||||
toolName: fullChunk.toolName,
|
||||
toolCallId: fullChunk.toolCallId,
|
||||
args: fullChunk.args,
|
||||
};
|
||||
}
|
||||
|
||||
if (fullChunk.type === "error") {
|
||||
logger.error(`LLM error: ${JSON.stringify(fullChunk)}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`LLM generation error: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
export interface DeepSearchPayload {
|
||||
content: string;
|
||||
userId: string;
|
||||
stream: boolean;
|
||||
intentOverride?: string;
|
||||
metadata?: {
|
||||
source?: "chrome" | "obsidian" | "mcp";
|
||||
url?: string;
|
||||
pageTitle?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface DeepSearchResponse {
|
||||
synthesis: string;
|
||||
episodes?: Array<{
|
||||
content: string;
|
||||
createdAt: Date;
|
||||
spaceIds: string[];
|
||||
}>;
|
||||
}
|
||||
@ -1,64 +0,0 @@
|
||||
import { tool } from "ai";
|
||||
import { z } from "zod";
|
||||
import axios from "axios";
|
||||
import { logger } from "@trigger.dev/sdk/v3";
|
||||
|
||||
export function createSearchMemoryTool(token: string) {
|
||||
return tool({
|
||||
description:
|
||||
"Search the user's memory for relevant facts and episodes. Use this tool multiple times with different queries to gather comprehensive context.",
|
||||
parameters: z.object({
|
||||
query: z
|
||||
.string()
|
||||
.describe(
|
||||
"Search query to find relevant information. Be specific: entity names, topics, concepts.",
|
||||
),
|
||||
}),
|
||||
execute: async ({ query }) => {
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${process.env.API_BASE_URL || "https://core.heysol.ai"}/api/v1/search`,
|
||||
{ query },
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const searchResult = response.data;
|
||||
|
||||
return {
|
||||
facts: searchResult.facts || [],
|
||||
episodes: searchResult.episodes || [],
|
||||
summary: `Found ${searchResult.episodes?.length || 0} relevant memories`,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`SearchMemory tool error: ${error}`);
|
||||
return {
|
||||
facts: [],
|
||||
episodes: [],
|
||||
summary: "No results found",
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Helper to extract unique episodes from tool calls
|
||||
export function extractEpisodesFromToolCalls(toolCalls: any[]): any[] {
|
||||
const episodes: any[] = [];
|
||||
|
||||
for (const call of toolCalls || []) {
|
||||
if (call.toolName === "searchMemory" && call.result?.episodes) {
|
||||
episodes.push(...call.result.episodes);
|
||||
}
|
||||
}
|
||||
|
||||
// Deduplicate by content + createdAt
|
||||
const uniqueEpisodes = Array.from(
|
||||
new Map(episodes.map((e) => [`${e.content}-${e.createdAt}`, e])).values(),
|
||||
);
|
||||
|
||||
return uniqueEpisodes.slice(0, 10);
|
||||
}
|
||||
@ -1,163 +1,11 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { logger } from "@trigger.dev/sdk/v3";
|
||||
import { jsonSchema, tool, type ToolSet } from "ai";
|
||||
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
||||
import { prisma } from "./prisma";
|
||||
|
||||
export const configureStdioMCPEnvironment = (
|
||||
spec: any,
|
||||
account: any,
|
||||
): { env: Record<string, string>; args: any[] } => {
|
||||
if (!spec.mcp) {
|
||||
return { env: {}, args: [] };
|
||||
}
|
||||
|
||||
const mcpSpec = spec.mcp;
|
||||
const configuredMCP = { ...mcpSpec };
|
||||
|
||||
// Replace config placeholders in environment variables
|
||||
if (configuredMCP.env) {
|
||||
for (const [key, value] of Object.entries(configuredMCP.env)) {
|
||||
if (typeof value === "string" && value.includes("${config:")) {
|
||||
// Extract the config key from the placeholder
|
||||
const configKey = value.match(/\$\{config:(.*?)\}/)?.[1];
|
||||
if (
|
||||
configKey &&
|
||||
account.integrationConfiguration &&
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(account.integrationConfiguration as any)[configKey]
|
||||
) {
|
||||
configuredMCP.env[key] = value.replace(
|
||||
`\${config:${configKey}}`,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(account.integrationConfiguration as any)[configKey],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof value === "string" && value.includes("${integrationConfig:")) {
|
||||
// Extract the config key from the placeholder
|
||||
const configKey = value.match(/\$\{integrationConfig:(.*?)\}/)?.[1];
|
||||
if (
|
||||
configKey &&
|
||||
account.integrationDefinition.config &&
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(account.integrationDefinition.config as any)[configKey]
|
||||
) {
|
||||
configuredMCP.env[key] = value.replace(
|
||||
`\${integrationConfig:${configKey}}`,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(account.integrationDefinition.config as any)[configKey],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
env: configuredMCP.env || {},
|
||||
args: Array.isArray(configuredMCP.args) ? configuredMCP.args : [],
|
||||
};
|
||||
};
|
||||
|
||||
export class MCP {
|
||||
private Client: any;
|
||||
private client: any = {};
|
||||
|
||||
constructor() {}
|
||||
|
||||
public async init() {
|
||||
this.Client = await MCP.importClient();
|
||||
}
|
||||
|
||||
private static async importClient() {
|
||||
const { Client } = await import(
|
||||
"@modelcontextprotocol/sdk/client/index.js"
|
||||
);
|
||||
return Client;
|
||||
}
|
||||
|
||||
async load(headers: any) {
|
||||
return await this.connectToServer(
|
||||
`${process.env.API_BASE_URL}/api/v1/mcp?source=core`,
|
||||
headers,
|
||||
);
|
||||
}
|
||||
|
||||
async allTools(): Promise<ToolSet> {
|
||||
try {
|
||||
const { tools } = await this.client.listTools();
|
||||
|
||||
const finalTools: ToolSet = {};
|
||||
|
||||
tools.map(({ name, description, inputSchema }: any) => {
|
||||
finalTools[name] = tool({
|
||||
description,
|
||||
parameters: jsonSchema(inputSchema),
|
||||
});
|
||||
});
|
||||
|
||||
return finalTools;
|
||||
} catch (error) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Flatten and convert to object
|
||||
}
|
||||
|
||||
async getTool(name: string) {
|
||||
try {
|
||||
const { tools: clientTools } = await this.client.listTools();
|
||||
const clientTool = clientTools.find((to: any) => to.name === name);
|
||||
|
||||
return JSON.stringify(clientTool);
|
||||
} catch (e) {
|
||||
logger.error((e as string) ?? "Getting tool failed");
|
||||
throw new Error("Getting tool failed");
|
||||
}
|
||||
}
|
||||
|
||||
async callTool(name: string, parameters: any) {
|
||||
const response = await this.client.callTool({
|
||||
name,
|
||||
arguments: parameters,
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async connectToServer(url: string, headers: any) {
|
||||
try {
|
||||
const client = new this.Client(
|
||||
{
|
||||
name: "Core",
|
||||
version: "1.0.0",
|
||||
},
|
||||
{
|
||||
capabilities: {},
|
||||
},
|
||||
);
|
||||
|
||||
// Configure the transport for MCP server
|
||||
const transport = new StreamableHTTPClientTransport(new URL(url), {
|
||||
requestInit: { headers },
|
||||
});
|
||||
|
||||
// Connect to the MCP server
|
||||
await client.connect(transport, { timeout: 60 * 1000 * 5 });
|
||||
this.client = client;
|
||||
|
||||
logger.info(`Connected to MCP server`);
|
||||
} catch (e) {
|
||||
logger.error(`Failed to connect to MCP server: `, { e });
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const fetchAndSaveStdioIntegrations = async () => {
|
||||
try {
|
||||
logger.info("Starting stdio integrations fetch and save process");
|
||||
|
||||
@ -8,11 +8,9 @@ import {
|
||||
type Workspace,
|
||||
} from "@prisma/client";
|
||||
|
||||
import { logger } from "@trigger.dev/sdk/v3";
|
||||
import { type CoreMessage } from "ai";
|
||||
|
||||
import { type HistoryStep } from "./types";
|
||||
import axios from "axios";
|
||||
import nodeCrypto from "node:crypto";
|
||||
import { customAlphabet, nanoid } from "nanoid";
|
||||
import { prisma } from "./prisma";
|
||||
@ -148,58 +146,6 @@ export interface RunChatPayload {
|
||||
isContinuation?: boolean;
|
||||
}
|
||||
|
||||
export const init = async ({ payload }: { payload: InitChatPayload }) => {
|
||||
logger.info("Loading init");
|
||||
const conversationHistory = await prisma.conversationHistory.findUnique({
|
||||
where: { id: payload.conversationHistoryId },
|
||||
include: { conversation: true },
|
||||
});
|
||||
|
||||
const conversation = conversationHistory?.conversation as Conversation;
|
||||
|
||||
const workspace = await prisma.workspace.findUnique({
|
||||
where: { id: conversation.workspaceId as string },
|
||||
});
|
||||
|
||||
if (!workspace) {
|
||||
return { conversation, conversationHistory };
|
||||
}
|
||||
|
||||
const randomKeyName = `chat_${nanoid(10)}`;
|
||||
const pat = await getOrCreatePersonalAccessToken({
|
||||
name: randomKeyName,
|
||||
userId: workspace.userId as string,
|
||||
});
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: { id: workspace.userId as string },
|
||||
});
|
||||
|
||||
// Set up axios interceptor for memory operations
|
||||
axios.interceptors.request.use((config) => {
|
||||
if (config.url?.startsWith("https://core::memory")) {
|
||||
// Handle both search and ingest endpoints
|
||||
config.url = config.url.replace(
|
||||
"https://core::memory",
|
||||
process.env.API_BASE_URL ?? "",
|
||||
);
|
||||
|
||||
config.headers.Authorization = `Bearer ${pat.token}`;
|
||||
}
|
||||
|
||||
return config;
|
||||
});
|
||||
|
||||
return {
|
||||
conversation,
|
||||
conversationHistory,
|
||||
tokenId: pat.id,
|
||||
token: pat.token,
|
||||
userId: user?.id,
|
||||
userName: user?.name,
|
||||
};
|
||||
};
|
||||
|
||||
export const createConversationHistoryForAgent = async (
|
||||
conversationId: string,
|
||||
) => {
|
||||
|
||||
@ -3,7 +3,6 @@ import { addToQueue } from "~/lib/ingest.server";
|
||||
import { logger } from "~/services/logger.service";
|
||||
import { SearchService } from "~/services/search.server";
|
||||
import { SpaceService } from "~/services/space.server";
|
||||
import { deepSearch } from "~/trigger/deep-search";
|
||||
import { IntegrationLoader } from "./integration-loader";
|
||||
import { hasCredits } from "~/services/billing.server";
|
||||
import { prisma } from "~/db.server";
|
||||
@ -229,8 +228,8 @@ export async function callMemoryTool(
|
||||
return await handleGetIntegrationActions({ ...args });
|
||||
case "execute_integration_action":
|
||||
return await handleExecuteIntegrationAction({ ...args });
|
||||
case "memory_deep_search":
|
||||
return await handleMemoryDeepSearch({ ...args, userId, source });
|
||||
// case "memory_deep_search":
|
||||
// return await handleMemoryDeepSearch({ ...args, userId, source });
|
||||
default:
|
||||
throw new Error(`Unknown memory tool: ${toolName}`);
|
||||
}
|
||||
@ -596,58 +595,3 @@ async function handleExecuteIntegrationAction(args: any) {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Handler for memory_deep_search
|
||||
async function handleMemoryDeepSearch(args: any) {
|
||||
try {
|
||||
const { content, intentOverride, userId, source } = args;
|
||||
|
||||
if (!content) {
|
||||
throw new Error("content is required");
|
||||
}
|
||||
|
||||
// Trigger non-streaming deep search task
|
||||
const handle = await deepSearch.triggerAndWait({
|
||||
content,
|
||||
userId,
|
||||
stream: false, // MCP doesn't need streaming
|
||||
intentOverride,
|
||||
metadata: { source },
|
||||
});
|
||||
|
||||
// Wait for task completion
|
||||
if (handle.ok) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(handle.output),
|
||||
},
|
||||
],
|
||||
isError: false,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error performing deep search: ${handle.error instanceof Error ? handle.error.message : String(handle.error)}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`MCP deep search error: ${error}`);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error performing deep search: ${error instanceof Error ? error.message : String(error)}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,11 +14,12 @@
|
||||
"trigger:deploy": "pnpm dlx trigger.dev@4.0.4 deploy"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/amazon-bedrock": "2.2.12",
|
||||
"@ai-sdk/anthropic": "^1.2.12",
|
||||
"@ai-sdk/google": "^1.2.22",
|
||||
"@ai-sdk/openai": "^1.3.21",
|
||||
"@anthropic-ai/sdk": "^0.60.0",
|
||||
"@ai-sdk/amazon-bedrock": "3.0.47",
|
||||
"@ai-sdk/anthropic": "^2.0.37",
|
||||
"@ai-sdk/google": "^2.0.23",
|
||||
"@ai-sdk/openai": "^2.0.53",
|
||||
"@ai-sdk/react": "2.0.78",
|
||||
"@anthropic-ai/sdk": "^0.67.0",
|
||||
"@aws-sdk/client-s3": "3.879.0",
|
||||
"@aws-sdk/credential-providers": "^3.894.0",
|
||||
"@aws-sdk/s3-request-presigner": "3.879.0",
|
||||
@ -81,7 +82,7 @@
|
||||
"@tiptap/starter-kit": "2.11.9",
|
||||
"@trigger.dev/react-hooks": "4.0.4",
|
||||
"@trigger.dev/sdk": "4.0.4",
|
||||
"ai": "4.3.19",
|
||||
"ai": "5.0.78",
|
||||
"axios": "^1.10.0",
|
||||
"bullmq": "^5.53.2",
|
||||
"cheerio": "^1.1.2",
|
||||
@ -117,7 +118,7 @@
|
||||
"neo4j-driver": "^5.28.1",
|
||||
"non.geist": "^1.0.2",
|
||||
"novel": "^1.0.2",
|
||||
"ollama-ai-provider": "1.2.0",
|
||||
"ollama-ai-provider-v2": "1.5.1",
|
||||
"openai": "^5.12.2",
|
||||
"posthog-js": "^1.116.6",
|
||||
"posthog-node": "^5.10.3",
|
||||
@ -128,6 +129,7 @@
|
||||
"react-resizable-panels": "^1.0.9",
|
||||
"react-hotkeys-hook": "^4.5.0",
|
||||
"react-virtualized": "^9.22.6",
|
||||
"resumable-stream": "2.2.8",
|
||||
"remix-auth": "^4.2.0",
|
||||
"remix-auth-oauth2": "^3.4.1",
|
||||
"remix-themes": "^2.0.4",
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
VERSION=0.1.25
|
||||
VERSION=0.1.26
|
||||
|
||||
# Nest run in docker, change host to database container name
|
||||
DB_HOST=postgres
|
||||
@ -48,4 +48,8 @@ OLLAMA_URL=http://ollama:11434
|
||||
EMBEDDING_MODEL=text-embedding-3-small
|
||||
MODEL=gpt-4.1-2025-04-14
|
||||
|
||||
|
||||
## for opensource embedding model
|
||||
# EMBEDDING_MODEL=mxbai-embed-large
|
||||
|
||||
QUEUE_PROVIDER=bullmq
|
||||
@ -109,24 +109,25 @@ services:
|
||||
retries: 10
|
||||
start_period: 20s
|
||||
|
||||
ollama:
|
||||
container_name: core-ollama
|
||||
image: ollama/ollama:0.12.6
|
||||
ports:
|
||||
- "11434:11434"
|
||||
volumes:
|
||||
- ollama_data:/root/.ollama
|
||||
- ./scripts/ollama-init.sh:/usr/local/bin/ollama-init.sh:ro
|
||||
networks:
|
||||
- core
|
||||
entrypoint: ["/bin/bash", "/usr/local/bin/ollama-init.sh"]
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:11434/api/tags"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 90s
|
||||
restart: unless-stopped
|
||||
# Uncomment this if you want to use a local embedding modal
|
||||
# ollama:
|
||||
# container_name: core-ollama
|
||||
# image: ollama/ollama:0.12.6
|
||||
# ports:
|
||||
# - "11434:11434"
|
||||
# volumes:
|
||||
# - ollama_data:/root/.ollama
|
||||
# - ./scripts/ollama-init.sh:/usr/local/bin/ollama-init.sh:ro
|
||||
# networks:
|
||||
# - core
|
||||
# entrypoint: ["/bin/bash", "/usr/local/bin/ollama-init.sh"]
|
||||
# healthcheck:
|
||||
# test: ["CMD", "curl", "-f", "http://localhost:11434/api/tags"]
|
||||
# interval: 30s
|
||||
# timeout: 10s
|
||||
# retries: 5
|
||||
# start_period: 90s
|
||||
# restart: unless-stopped
|
||||
|
||||
networks:
|
||||
core:
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "core",
|
||||
"private": true,
|
||||
"version": "0.1.25",
|
||||
"version": "0.1.26",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
|
||||
274
pnpm-lock.yaml
generated
274
pnpm-lock.yaml
generated
@ -281,20 +281,23 @@ importers:
|
||||
apps/webapp:
|
||||
dependencies:
|
||||
'@ai-sdk/amazon-bedrock':
|
||||
specifier: 2.2.12
|
||||
version: 2.2.12(zod@3.25.76)
|
||||
specifier: 3.0.47
|
||||
version: 3.0.47(zod@3.25.76)
|
||||
'@ai-sdk/anthropic':
|
||||
specifier: ^1.2.12
|
||||
version: 1.2.12(zod@3.25.76)
|
||||
specifier: ^2.0.37
|
||||
version: 2.0.37(zod@3.25.76)
|
||||
'@ai-sdk/google':
|
||||
specifier: ^1.2.22
|
||||
version: 1.2.22(zod@3.25.76)
|
||||
specifier: ^2.0.23
|
||||
version: 2.0.23(zod@3.25.76)
|
||||
'@ai-sdk/openai':
|
||||
specifier: ^1.3.21
|
||||
version: 1.3.22(zod@3.25.76)
|
||||
specifier: ^2.0.53
|
||||
version: 2.0.53(zod@3.25.76)
|
||||
'@ai-sdk/react':
|
||||
specifier: 2.0.78
|
||||
version: 2.0.78(react@18.3.1)(zod@3.25.76)
|
||||
'@anthropic-ai/sdk':
|
||||
specifier: ^0.60.0
|
||||
version: 0.60.0
|
||||
specifier: ^0.67.0
|
||||
version: 0.67.0(zod@3.25.76)
|
||||
'@aws-sdk/client-s3':
|
||||
specifier: 3.879.0
|
||||
version: 3.879.0
|
||||
@ -480,10 +483,10 @@ importers:
|
||||
version: 4.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@trigger.dev/sdk':
|
||||
specifier: 4.0.4
|
||||
version: 4.0.4(ai@4.3.19(react@18.3.1)(zod@3.25.76))(zod@3.25.76)
|
||||
version: 4.0.4(ai@5.0.78(zod@3.25.76))(zod@3.25.76)
|
||||
ai:
|
||||
specifier: 4.3.19
|
||||
version: 4.3.19(react@18.3.1)(zod@3.25.76)
|
||||
specifier: 5.0.78
|
||||
version: 5.0.78(zod@3.25.76)
|
||||
axios:
|
||||
specifier: ^1.10.0
|
||||
version: 1.10.0
|
||||
@ -589,9 +592,9 @@ importers:
|
||||
novel:
|
||||
specifier: ^1.0.2
|
||||
version: 1.0.2(@tiptap/extension-code-block@2.11.9(@tiptap/core@2.25.0(@tiptap/pm@2.25.0))(@tiptap/pm@2.25.0))(@types/react-dom@18.3.7(@types/react@18.2.69))(@types/react@18.2.69)(highlight.js@11.11.1)(lowlight@3.3.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
ollama-ai-provider:
|
||||
specifier: 1.2.0
|
||||
version: 1.2.0(zod@3.25.76)
|
||||
ollama-ai-provider-v2:
|
||||
specifier: 1.5.1
|
||||
version: 1.5.1(zod@3.25.76)
|
||||
openai:
|
||||
specifier: ^5.12.2
|
||||
version: 5.12.2(ws@8.18.3)(zod@3.25.76)
|
||||
@ -637,6 +640,9 @@ importers:
|
||||
remix-utils:
|
||||
specifier: ^7.7.0
|
||||
version: 7.7.0(@remix-run/node@2.1.0(typescript@5.8.3))(@remix-run/react@2.16.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@remix-run/router@1.23.0)(crypto-js@4.2.0)(react@18.3.1)(zod@3.25.76)
|
||||
resumable-stream:
|
||||
specifier: 2.2.8
|
||||
version: 2.2.8
|
||||
sigma:
|
||||
specifier: ^3.0.2
|
||||
version: 3.0.2(graphology-types@0.24.8)
|
||||
@ -928,56 +934,56 @@ importers:
|
||||
|
||||
packages:
|
||||
|
||||
'@ai-sdk/amazon-bedrock@2.2.12':
|
||||
resolution: {integrity: sha512-m8gARnh45pr1s08Uu4J/Pm8913mwJPejPOm59b+kUqMsP9ilhUtH/bp8432Ra/v+vHuMoBrglG2ZvXtctAaH2g==}
|
||||
'@ai-sdk/amazon-bedrock@3.0.47':
|
||||
resolution: {integrity: sha512-oTAxTU4k1+EIKP41nvLGN7dWwoK7dg1JptrX6csn7abmSfQSsygDrfeMf8/7Mdnr+frt9i5ogvpQkp1ak0916Q==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.0.0
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
'@ai-sdk/anthropic@1.2.12':
|
||||
resolution: {integrity: sha512-YSzjlko7JvuiyQFmI9RN1tNZdEiZxc+6xld/0tq/VkJaHpEzGAb1yiNxxvmYVcjvfu/PcvCxAAYXmTYQQ63IHQ==}
|
||||
'@ai-sdk/anthropic@2.0.37':
|
||||
resolution: {integrity: sha512-r2e9BWoobisH9B5b7x3yYG/k9WlsZqa4D94o7gkwktReqrjjv83zNMop4KmlJsh/zBhbsaP8S8SUfiwK+ESxgg==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.0.0
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
'@ai-sdk/google@1.2.22':
|
||||
resolution: {integrity: sha512-Ppxu3DIieF1G9pyQ5O1Z646GYR0gkC57YdBqXJ82qvCdhEhZHu0TWhmnOoeIWe2olSbuDeoOY+MfJrW8dzS3Hw==}
|
||||
'@ai-sdk/gateway@2.0.1':
|
||||
resolution: {integrity: sha512-vPVIbnP35ZnayS937XLo85vynR85fpBQWHCdUweq7apzqFOTU2YkUd4V3msebEHbQ2Zro60ZShDDy9SMiyWTqA==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.0.0
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
'@ai-sdk/openai@1.3.22':
|
||||
resolution: {integrity: sha512-QwA+2EkG0QyjVR+7h6FE7iOu2ivNqAVMm9UJZkVxxTk5OIq5fFJDTEI/zICEMuHImTTXR2JjsL6EirJ28Jc4cw==}
|
||||
'@ai-sdk/google@2.0.23':
|
||||
resolution: {integrity: sha512-VbCnKR+6aWUVLkAiSW5gUEtST7KueEmlt+d6qwDikxlLnFG9pzy59je8MiDVeM5G2tuSXbvZQF78PGIfXDBmow==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.0.0
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
'@ai-sdk/provider-utils@2.2.8':
|
||||
resolution: {integrity: sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA==}
|
||||
'@ai-sdk/openai@2.0.53':
|
||||
resolution: {integrity: sha512-GIkR3+Fyif516ftXv+YPSPstnAHhcZxNoR2s8uSHhQ1yBT7I7aQYTVwpjAuYoT3GR+TeP50q7onj2/nDRbT2FQ==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.23.8
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
'@ai-sdk/provider@1.1.3':
|
||||
resolution: {integrity: sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==}
|
||||
'@ai-sdk/provider-utils@3.0.12':
|
||||
resolution: {integrity: sha512-ZtbdvYxdMoria+2SlNarEk6Hlgyf+zzcznlD55EAl+7VZvJaSg2sqPvwArY7L6TfDEDJsnCq0fdhBSkYo0Xqdg==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
'@ai-sdk/provider@2.0.0':
|
||||
resolution: {integrity: sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@ai-sdk/react@1.2.12':
|
||||
resolution: {integrity: sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g==}
|
||||
'@ai-sdk/react@2.0.78':
|
||||
resolution: {integrity: sha512-f5inDBHJyUEzbtNxc9HiTxbcGjtot0uuc//0/khGrl8IZlLxw+yTxO/T1Qq95Rw5QPwTx9/Aw7wIZei3qws9hA==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
react: ^18 || ^19 || ^19.0.0-rc
|
||||
zod: ^3.23.8
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
peerDependenciesMeta:
|
||||
zod:
|
||||
optional: true
|
||||
|
||||
'@ai-sdk/ui-utils@1.2.11':
|
||||
resolution: {integrity: sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.23.8
|
||||
|
||||
'@alloc/quick-lru@5.2.0':
|
||||
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
||||
engines: {node: '>=10'}
|
||||
@ -986,9 +992,14 @@ packages:
|
||||
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
'@anthropic-ai/sdk@0.60.0':
|
||||
resolution: {integrity: sha512-9zu/TXaUy8BZhXedDtt1wT3H4LOlpKDO1/ftiFpeR3N1PCr3KJFKkxxlQWWt1NNp08xSwUNJ3JNY8yhl8av6eQ==}
|
||||
'@anthropic-ai/sdk@0.67.0':
|
||||
resolution: {integrity: sha512-Buxbf6jYJ+pPtfCgXe1pcFtZmdXPrbdqhBjiscFt9irS1G0hCsmR/fPA+DwKTk4GPjqeNnnCYNecXH6uVZ4G/A==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
zod: ^3.25.0 || ^4.0.0
|
||||
peerDependenciesMeta:
|
||||
zod:
|
||||
optional: true
|
||||
|
||||
'@arr/every@1.0.1':
|
||||
resolution: {integrity: sha512-UQFQ6SgyJ6LX42W8rHCs8KVc0JS0tzVL9ct4XYedJukskYVWTo49tNiMEK9C2HTyarbNiT/RVIRSY82vH+6sTg==}
|
||||
@ -5108,6 +5119,9 @@ packages:
|
||||
'@socket.io/component-emitter@3.1.2':
|
||||
resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==}
|
||||
|
||||
'@standard-schema/spec@1.0.0':
|
||||
resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
|
||||
|
||||
'@swc/core-darwin-arm64@1.3.101':
|
||||
resolution: {integrity: sha512-mNFK+uHNPRXSnfTOG34zJOeMl2waM4hF4a2NY7dkMXrPqw9CoJn4MwTXJcyMiSz1/BnNjjTCHF3Yhj0jPxmkzQ==}
|
||||
engines: {node: '>=10'}
|
||||
@ -5685,9 +5699,6 @@ packages:
|
||||
'@types/debug@4.1.12':
|
||||
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
||||
|
||||
'@types/diff-match-patch@1.0.36':
|
||||
resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==}
|
||||
|
||||
'@types/eslint-scope@3.7.7':
|
||||
resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==}
|
||||
|
||||
@ -6121,6 +6132,10 @@ packages:
|
||||
'@vanilla-extract/private@1.0.8':
|
||||
resolution: {integrity: sha512-oRAbUlq1SyTWCo7dQnTVm+xgJMqNl8K1dEempQHXzQvUuyEfBabMt0wNGf+VCHzvKbx/Bzr9p/2wy8WA9+2z2g==}
|
||||
|
||||
'@vercel/oidc@3.0.3':
|
||||
resolution: {integrity: sha512-yNEQvPcVrK9sIe637+I0jD6leluPxzwJKx/Haw6F4H77CdDsszUn5V3o96LPziXkSNE2B83+Z3mjqGKBK/R6Gg==}
|
||||
engines: {node: '>= 20'}
|
||||
|
||||
'@web3-storage/multipart-parser@1.0.0':
|
||||
resolution: {integrity: sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw==}
|
||||
|
||||
@ -6229,15 +6244,11 @@ packages:
|
||||
resolution: {integrity: sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
ai@4.3.19:
|
||||
resolution: {integrity: sha512-dIE2bfNpqHN3r6IINp9znguYdhIOheKW2LDigAMrgt/upT3B8eBGPSCblENvaZGoq+hxaN9fSMzjWpbqloP+7Q==}
|
||||
ai@5.0.78:
|
||||
resolution: {integrity: sha512-ec77fmQwJGLduswMrW4AAUGSOiu8dZaIwMmWHHGKsrMUFFS6ugfkTyx0srtuKYHNRRLRC2dT7cPirnUl98VnxA==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
react: ^18 || ^19 || ^19.0.0-rc
|
||||
zod: ^3.23.8
|
||||
peerDependenciesMeta:
|
||||
react:
|
||||
optional: true
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
ajv-formats@2.1.1:
|
||||
resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
|
||||
@ -7286,9 +7297,6 @@ packages:
|
||||
didyoumean@1.2.2:
|
||||
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
|
||||
|
||||
diff-match-patch@1.0.5:
|
||||
resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==}
|
||||
|
||||
diff@5.2.0:
|
||||
resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==}
|
||||
engines: {node: '>=0.3.1'}
|
||||
@ -7775,6 +7783,10 @@ packages:
|
||||
resolution: {integrity: sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
eventsource-parser@3.0.6:
|
||||
resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
eventsource@3.0.7:
|
||||
resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
@ -8730,6 +8742,10 @@ packages:
|
||||
resolution: {integrity: sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==}
|
||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||
|
||||
json-schema-to-ts@3.1.1:
|
||||
resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
json-schema-traverse@0.4.1:
|
||||
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
|
||||
|
||||
@ -8754,11 +8770,6 @@ packages:
|
||||
jsonc-parser@3.2.1:
|
||||
resolution: {integrity: sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==}
|
||||
|
||||
jsondiffpatch@0.6.0:
|
||||
resolution: {integrity: sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
|
||||
jsonfile@4.0.0:
|
||||
resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==}
|
||||
|
||||
@ -9726,14 +9737,11 @@ packages:
|
||||
ohash@1.1.6:
|
||||
resolution: {integrity: sha512-TBu7PtV8YkAZn0tSxobKY2n2aAQva936lhRrj6957aDaCf9IEtqsKbgMzXE/F/sjqYOwmrukeORHNLe5glk7Cg==}
|
||||
|
||||
ollama-ai-provider@1.2.0:
|
||||
resolution: {integrity: sha512-jTNFruwe3O/ruJeppI/quoOUxG7NA6blG3ZyQj3lei4+NnJo7bi3eIRWqlVpRlu/mbzbFXeJSBuYQWF6pzGKww==}
|
||||
ollama-ai-provider-v2@1.5.1:
|
||||
resolution: {integrity: sha512-5R3z7Y+mm8VEtoq+rIoIqkEy83oYM3DXX6Nyrn6yofYvYl56BCoJMNwXsPrpmCI0O4fN/gAIDTLpznYMRGzZ5g==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.0.0
|
||||
peerDependenciesMeta:
|
||||
zod:
|
||||
optional: true
|
||||
zod: ^4.0.16
|
||||
|
||||
on-finished@2.3.0:
|
||||
resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==}
|
||||
@ -9923,9 +9931,6 @@ packages:
|
||||
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
partial-json@0.1.7:
|
||||
resolution: {integrity: sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==}
|
||||
|
||||
partysocket@1.1.4:
|
||||
resolution: {integrity: sha512-jXP7PFj2h5/v4UjDS8P7MZy6NJUQ7sspiFyxL4uc/+oKOL+KdtXzHnTV8INPGxBrLTXgalyG3kd12Qm7WrYc3A==}
|
||||
|
||||
@ -10923,6 +10928,9 @@ packages:
|
||||
resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
resumable-stream@2.2.8:
|
||||
resolution: {integrity: sha512-F9+SLKw/a/p7hRjy2CNwzT66UIlY7aY4D3Sg9xwuZMA7nxVQrVPXCWU27qIGcO4jlauL0T3XkCN2218qi6ugTw==}
|
||||
|
||||
retry@0.12.0:
|
||||
resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==}
|
||||
engines: {node: '>= 4'}
|
||||
@ -11020,9 +11028,6 @@ packages:
|
||||
resolution: {integrity: sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==}
|
||||
engines: {node: '>= 10.13.0'}
|
||||
|
||||
secure-json-parse@2.7.0:
|
||||
resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==}
|
||||
|
||||
selderee@0.11.0:
|
||||
resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==}
|
||||
|
||||
@ -11610,6 +11615,9 @@ packages:
|
||||
resolution: {integrity: sha512-kr8SKKw94OI+xTGOkfsvwZQ8mWoikZDd2n8XZHjJVZUARZT+4/VV6cacRS6CLsH9bNm+HFIPU1Zx4CnNnb4qlQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
ts-algebra@2.0.0:
|
||||
resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==}
|
||||
|
||||
ts-api-utils@1.4.3:
|
||||
resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==}
|
||||
engines: {node: '>=16'}
|
||||
@ -12381,61 +12389,62 @@ packages:
|
||||
|
||||
snapshots:
|
||||
|
||||
'@ai-sdk/amazon-bedrock@2.2.12(zod@3.25.76)':
|
||||
'@ai-sdk/amazon-bedrock@3.0.47(zod@3.25.76)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 1.1.3
|
||||
'@ai-sdk/provider-utils': 2.2.8(zod@3.25.76)
|
||||
'@ai-sdk/anthropic': 2.0.37(zod@3.25.76)
|
||||
'@ai-sdk/provider': 2.0.0
|
||||
'@ai-sdk/provider-utils': 3.0.12(zod@3.25.76)
|
||||
'@smithy/eventstream-codec': 4.1.1
|
||||
'@smithy/util-utf8': 4.1.0
|
||||
aws4fetch: 1.0.20
|
||||
zod: 3.25.76
|
||||
|
||||
'@ai-sdk/anthropic@1.2.12(zod@3.25.76)':
|
||||
'@ai-sdk/anthropic@2.0.37(zod@3.25.76)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 1.1.3
|
||||
'@ai-sdk/provider-utils': 2.2.8(zod@3.25.76)
|
||||
'@ai-sdk/provider': 2.0.0
|
||||
'@ai-sdk/provider-utils': 3.0.12(zod@3.25.76)
|
||||
zod: 3.25.76
|
||||
|
||||
'@ai-sdk/google@1.2.22(zod@3.25.76)':
|
||||
'@ai-sdk/gateway@2.0.1(zod@3.25.76)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 1.1.3
|
||||
'@ai-sdk/provider-utils': 2.2.8(zod@3.25.76)
|
||||
'@ai-sdk/provider': 2.0.0
|
||||
'@ai-sdk/provider-utils': 3.0.12(zod@3.25.76)
|
||||
'@vercel/oidc': 3.0.3
|
||||
zod: 3.25.76
|
||||
|
||||
'@ai-sdk/openai@1.3.22(zod@3.25.76)':
|
||||
'@ai-sdk/google@2.0.23(zod@3.25.76)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 1.1.3
|
||||
'@ai-sdk/provider-utils': 2.2.8(zod@3.25.76)
|
||||
'@ai-sdk/provider': 2.0.0
|
||||
'@ai-sdk/provider-utils': 3.0.12(zod@3.25.76)
|
||||
zod: 3.25.76
|
||||
|
||||
'@ai-sdk/provider-utils@2.2.8(zod@3.25.76)':
|
||||
'@ai-sdk/openai@2.0.53(zod@3.25.76)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 1.1.3
|
||||
nanoid: 3.3.8
|
||||
secure-json-parse: 2.7.0
|
||||
'@ai-sdk/provider': 2.0.0
|
||||
'@ai-sdk/provider-utils': 3.0.12(zod@3.25.76)
|
||||
zod: 3.25.76
|
||||
|
||||
'@ai-sdk/provider@1.1.3':
|
||||
'@ai-sdk/provider-utils@3.0.12(zod@3.25.76)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 2.0.0
|
||||
'@standard-schema/spec': 1.0.0
|
||||
eventsource-parser: 3.0.6
|
||||
zod: 3.25.76
|
||||
|
||||
'@ai-sdk/provider@2.0.0':
|
||||
dependencies:
|
||||
json-schema: 0.4.0
|
||||
|
||||
'@ai-sdk/react@1.2.12(react@18.3.1)(zod@3.25.76)':
|
||||
'@ai-sdk/react@2.0.78(react@18.3.1)(zod@3.25.76)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider-utils': 2.2.8(zod@3.25.76)
|
||||
'@ai-sdk/ui-utils': 1.2.11(zod@3.25.76)
|
||||
'@ai-sdk/provider-utils': 3.0.12(zod@3.25.76)
|
||||
ai: 5.0.78(zod@3.25.76)
|
||||
react: 18.3.1
|
||||
swr: 2.3.3(react@18.3.1)
|
||||
throttleit: 2.1.0
|
||||
optionalDependencies:
|
||||
zod: 3.25.76
|
||||
|
||||
'@ai-sdk/ui-utils@1.2.11(zod@3.25.76)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 1.1.3
|
||||
'@ai-sdk/provider-utils': 2.2.8(zod@3.25.76)
|
||||
zod: 3.25.76
|
||||
zod-to-json-schema: 3.24.5(zod@3.25.76)
|
||||
|
||||
'@alloc/quick-lru@5.2.0': {}
|
||||
|
||||
'@ampproject/remapping@2.3.0':
|
||||
@ -12443,7 +12452,11 @@ snapshots:
|
||||
'@jridgewell/gen-mapping': 0.3.8
|
||||
'@jridgewell/trace-mapping': 0.3.25
|
||||
|
||||
'@anthropic-ai/sdk@0.60.0': {}
|
||||
'@anthropic-ai/sdk@0.67.0(zod@3.25.76)':
|
||||
dependencies:
|
||||
json-schema-to-ts: 3.1.1
|
||||
optionalDependencies:
|
||||
zod: 3.25.76
|
||||
|
||||
'@arr/every@1.0.1': {}
|
||||
|
||||
@ -17989,6 +18002,8 @@ snapshots:
|
||||
|
||||
'@socket.io/component-emitter@3.1.2': {}
|
||||
|
||||
'@standard-schema/spec@1.0.0': {}
|
||||
|
||||
'@swc/core-darwin-arm64@1.3.101':
|
||||
optional: true
|
||||
|
||||
@ -18457,7 +18472,7 @@ snapshots:
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
'@trigger.dev/sdk@4.0.4(ai@4.3.19(react@18.3.1)(zod@3.25.76))(zod@3.25.76)':
|
||||
'@trigger.dev/sdk@4.0.4(ai@5.0.78(zod@3.25.76))(zod@3.25.76)':
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@opentelemetry/semantic-conventions': 1.36.0
|
||||
@ -18473,7 +18488,7 @@ snapshots:
|
||||
ws: 8.18.3
|
||||
zod: 3.25.76
|
||||
optionalDependencies:
|
||||
ai: 4.3.19(react@18.3.1)(zod@3.25.76)
|
||||
ai: 5.0.78(zod@3.25.76)
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- supports-color
|
||||
@ -18635,8 +18650,6 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/ms': 2.1.0
|
||||
|
||||
'@types/diff-match-patch@1.0.36': {}
|
||||
|
||||
'@types/eslint-scope@3.7.7':
|
||||
dependencies:
|
||||
'@types/eslint': 9.6.1
|
||||
@ -19146,6 +19159,8 @@ snapshots:
|
||||
|
||||
'@vanilla-extract/private@1.0.8': {}
|
||||
|
||||
'@vercel/oidc@3.0.3': {}
|
||||
|
||||
'@web3-storage/multipart-parser@1.0.0': {}
|
||||
|
||||
'@webassemblyjs/ast@1.14.1':
|
||||
@ -19277,17 +19292,13 @@ snapshots:
|
||||
clean-stack: 4.2.0
|
||||
indent-string: 5.0.0
|
||||
|
||||
ai@4.3.19(react@18.3.1)(zod@3.25.76):
|
||||
ai@5.0.78(zod@3.25.76):
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 1.1.3
|
||||
'@ai-sdk/provider-utils': 2.2.8(zod@3.25.76)
|
||||
'@ai-sdk/react': 1.2.12(react@18.3.1)(zod@3.25.76)
|
||||
'@ai-sdk/ui-utils': 1.2.11(zod@3.25.76)
|
||||
'@ai-sdk/gateway': 2.0.1(zod@3.25.76)
|
||||
'@ai-sdk/provider': 2.0.0
|
||||
'@ai-sdk/provider-utils': 3.0.12(zod@3.25.76)
|
||||
'@opentelemetry/api': 1.9.0
|
||||
jsondiffpatch: 0.6.0
|
||||
zod: 3.25.76
|
||||
optionalDependencies:
|
||||
react: 18.3.1
|
||||
|
||||
ajv-formats@2.1.1(ajv@8.17.1):
|
||||
optionalDependencies:
|
||||
@ -20434,8 +20445,6 @@ snapshots:
|
||||
|
||||
didyoumean@1.2.2: {}
|
||||
|
||||
diff-match-patch@1.0.5: {}
|
||||
|
||||
diff@5.2.0: {}
|
||||
|
||||
dir-glob@3.0.1:
|
||||
@ -21225,6 +21234,8 @@ snapshots:
|
||||
|
||||
eventsource-parser@3.0.3: {}
|
||||
|
||||
eventsource-parser@3.0.6: {}
|
||||
|
||||
eventsource@3.0.7:
|
||||
dependencies:
|
||||
eventsource-parser: 3.0.3
|
||||
@ -22274,6 +22285,11 @@ snapshots:
|
||||
|
||||
json-parse-even-better-errors@3.0.2: {}
|
||||
|
||||
json-schema-to-ts@3.1.1:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.6
|
||||
ts-algebra: 2.0.0
|
||||
|
||||
json-schema-traverse@0.4.1: {}
|
||||
|
||||
json-schema-traverse@1.0.0: {}
|
||||
@ -22290,12 +22306,6 @@ snapshots:
|
||||
|
||||
jsonc-parser@3.2.1: {}
|
||||
|
||||
jsondiffpatch@0.6.0:
|
||||
dependencies:
|
||||
'@types/diff-match-patch': 1.0.36
|
||||
chalk: 5.4.1
|
||||
diff-match-patch: 1.0.5
|
||||
|
||||
jsonfile@4.0.0:
|
||||
optionalDependencies:
|
||||
graceful-fs: 4.2.11
|
||||
@ -23597,12 +23607,10 @@ snapshots:
|
||||
|
||||
ohash@1.1.6: {}
|
||||
|
||||
ollama-ai-provider@1.2.0(zod@3.25.76):
|
||||
ollama-ai-provider-v2@1.5.1(zod@3.25.76):
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 1.1.3
|
||||
'@ai-sdk/provider-utils': 2.2.8(zod@3.25.76)
|
||||
partial-json: 0.1.7
|
||||
optionalDependencies:
|
||||
'@ai-sdk/provider': 2.0.0
|
||||
'@ai-sdk/provider-utils': 3.0.12(zod@3.25.76)
|
||||
zod: 3.25.76
|
||||
|
||||
on-finished@2.3.0:
|
||||
@ -23801,8 +23809,6 @@ snapshots:
|
||||
|
||||
parseurl@1.3.3: {}
|
||||
|
||||
partial-json@0.1.7: {}
|
||||
|
||||
partysocket@1.1.4:
|
||||
dependencies:
|
||||
event-target-polyfill: 0.0.4
|
||||
@ -24912,6 +24918,8 @@ snapshots:
|
||||
onetime: 5.1.2
|
||||
signal-exit: 3.0.7
|
||||
|
||||
resumable-stream@2.2.8: {}
|
||||
|
||||
retry@0.12.0: {}
|
||||
|
||||
retry@0.13.1: {}
|
||||
@ -25039,8 +25047,6 @@ snapshots:
|
||||
ajv-formats: 2.1.1(ajv@8.17.1)
|
||||
ajv-keywords: 5.1.0(ajv@8.17.1)
|
||||
|
||||
secure-json-parse@2.7.0: {}
|
||||
|
||||
selderee@0.11.0:
|
||||
dependencies:
|
||||
parseley: 0.12.1
|
||||
@ -25794,6 +25800,8 @@ snapshots:
|
||||
dependencies:
|
||||
matchit: 1.1.0
|
||||
|
||||
ts-algebra@2.0.0: {}
|
||||
|
||||
ts-api-utils@1.4.3(typescript@5.8.3):
|
||||
dependencies:
|
||||
typescript: 5.8.3
|
||||
@ -26619,10 +26627,6 @@ snapshots:
|
||||
dependencies:
|
||||
zod: 3.23.8
|
||||
|
||||
zod-to-json-schema@3.24.5(zod@3.25.76):
|
||||
dependencies:
|
||||
zod: 3.25.76
|
||||
|
||||
zod-validation-error@1.5.0(zod@3.23.8):
|
||||
dependencies:
|
||||
zod: 3.23.8
|
||||
|
||||
@ -92,6 +92,8 @@
|
||||
"PRO_PLAN_CREDITS",
|
||||
"PRO_OVERAGE_PRICE",
|
||||
"MAX_PLAN_CREDITS",
|
||||
"MAX_OVERAGE_PRICE"
|
||||
"MAX_OVERAGE_PRICE",
|
||||
"TELEMETRY_ENABLED",
|
||||
"TELEMETRY_ANONYMOUS"
|
||||
]
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user