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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 58 additions & 1 deletion example/convex/_generated/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type * as chat_human from "../chat/human.js";
import type * as chat_streamAbort from "../chat/streamAbort.js";
import type * as chat_streaming from "../chat/streaming.js";
import type * as chat_streamingReasoning from "../chat/streamingReasoning.js";
import type * as coreMemories_utils from "../coreMemories/utils.js";
import type * as crons from "../crons.js";
import type * as debugging_rawRequestResponseHandler from "../debugging/rawRequestResponseHandler.js";
import type * as etc_objects from "../etc/objects.js";
Expand Down Expand Up @@ -71,6 +72,7 @@ declare const fullApi: ApiFromModules<{
"chat/streamAbort": typeof chat_streamAbort;
"chat/streaming": typeof chat_streaming;
"chat/streamingReasoning": typeof chat_streamingReasoning;
"coreMemories/utils": typeof coreMemories_utils;
crons: typeof crons;
"debugging/rawRequestResponseHandler": typeof debugging_rawRequestResponseHandler;
"etc/objects": typeof etc_objects;
Expand Down Expand Up @@ -135,6 +137,61 @@ export declare const components: {
boolean
>;
};
coreMemories: {
append: FunctionReference<
"mutation",
"internal",
{ field: "persona" | "human"; text: string; userId?: string },
null
>;
get: FunctionReference<
"query",
"internal",
{ userId?: string },
null | {
_creationTime: number;
_id: string;
human: string;
persona: string;
userId?: string;
}
>;
getOrCreate: FunctionReference<
"mutation",
"internal",
{ human: string; persona: string; userId?: string },
null | {
_creationTime: number;
_id: string;
human: string;
persona: string;
userId?: string;
}
>;
remove: FunctionReference<
"mutation",
"internal",
{ userId?: string },
null
>;
replace: FunctionReference<
"mutation",
"internal",
{
field: "persona" | "human";
newContent: string;
oldContent: string;
userId?: string;
},
number
>;
update: FunctionReference<
"mutation",
"internal",
{ human?: string; persona?: string; userId?: string },
null
>;
};
files: {
addFile: FunctionReference<
"mutation",
Expand Down Expand Up @@ -838,7 +895,7 @@ export declare const components: {
"mutation",
"internal",
{ messageIds: Array<string> },
Array<string>
any
>;
deleteByOrder: FunctionReference<
"mutation",
Expand Down
2 changes: 2 additions & 0 deletions example/convex/agents/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ export const defaultConfig = {
},
// If you want to use vector search, you need to set this.
textEmbeddingModel,
// Enable built-in memory tools (append/replace core memory, message search)
memoryTools: true,
} satisfies Config;
78 changes: 78 additions & 0 deletions example/convex/coreMemories/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { components } from "../_generated/api";
import { mutation, query } from "../_generated/server";
import { v } from "convex/values";
import { getAuthUserId } from "../utils";

export const get = query({
args: {},
handler: async (ctx) => {
const userId = await getAuthUserId(ctx);
return await ctx.runQuery(components.agent.coreMemories.get, { userId });
},
});

export const getOrCreate = mutation({
args: {},
handler: async (ctx) => {
const userId = await getAuthUserId(ctx);
return await ctx.runMutation(components.agent.coreMemories.getOrCreate, {
userId,
persona: "",
human: "",
});
},
});

export const update = mutation({
args: {
persona: v.optional(v.string()),
human: v.optional(v.string()),
},
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx);
await ctx.runMutation(components.agent.coreMemories.update, {
userId,
...args,
});
},
});

export const append = mutation({
args: {
field: v.union(v.literal("persona"), v.literal("human")),
text: v.string(),
},
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx);
await ctx.runMutation(components.agent.coreMemories.append, {
userId,
field: args.field,
text: args.text,
});
},
});

export const replace = mutation({
args: {
field: v.union(v.literal("persona"), v.literal("human")),
oldContent: v.string(),
newContent: v.string(),
},
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx);
return await ctx.runMutation(components.agent.coreMemories.replace, {
userId,
field: args.field,
oldContent: args.oldContent,
newContent: args.newContent,
});
},
});

export const remove = mutation({
args: {},
handler: async (ctx) => {
const userId = await getAuthUserId(ctx);
await ctx.runMutation(components.agent.coreMemories.remove, { userId });
},
});
8 changes: 7 additions & 1 deletion example/convex/modelsForDemo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import type { LanguageModelV2 } from "@ai-sdk/provider";
import { openai } from "@ai-sdk/openai";
import { groq } from "@ai-sdk/groq";
import { mockModel } from "@convex-dev/agent";
import { google } from "@ai-sdk/google";

let languageModel: LanguageModelV2;
let textEmbeddingModel: EmbeddingModel<string>;

if (process.env.OPENAI_API_KEY) {
languageModel = openai.chat("gpt-4o-mini");
textEmbeddingModel = openai.textEmbeddingModel("text-embedding-3-small");
} else if (process.env.GROQ_API_KEY) {
languageModel = groq.languageModel(
"meta-llama/llama-4-scout-17b-16e-instruct",
Expand All @@ -24,5 +24,11 @@ if (process.env.OPENAI_API_KEY) {
);
}

if (process.env.OPENAI_API_KEY) {
textEmbeddingModel = openai.textEmbeddingModel("text-embedding-3-small");
} else if (process.env.GOOGLE_GENERATIVE_AI_API_KEY) {
textEmbeddingModel = google.textEmbedding("gemini-embedding-001");
}

// If you want to use different models for examples, you can change them here.
export { languageModel, textEmbeddingModel };
34 changes: 34 additions & 0 deletions example/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"lint": "tsc -p convex && eslint convex"
},
"dependencies": {
"@ai-sdk/google": "^2.0.14",
"@ai-sdk/groq": "^2.0.0",
"@ai-sdk/openai": "^2.0.0",
"@ai-sdk/provider": "^2.0.0",
Expand Down
95 changes: 95 additions & 0 deletions example/ui/coreMemories/MemoryUI.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { useMutation, useQuery } from "convex/react";
import { api } from "../../convex/_generated/api";
import { useEffect, useState } from "react";

export default function MemoryUI() {
const ensureCore = useMutation(api.coreMemories.utils.getOrCreate);

useEffect(() => {
void ensureCore();
}, [ensureCore]);

return (
<div className="h-full flex flex-col">
<header className="sticky top-0 z-10 bg-white/80 backdrop-blur-sm p-4 flex justify-between items-center border-b">
<h1 className="text-xl font-semibold accent-text">Core Memories</h1>
</header>
<div className="h-[calc(100vh-8rem)] flex items-center justify-center p-8 bg-gray-50">
<div className="w-full max-w-4xl bg-white rounded-xl shadow-lg p-8">
<MemoryEditor />
</div>
</div>
</div>
);
}

function MemoryEditor() {
const mem = useQuery(api.coreMemories.utils.get, {});
const update = useMutation(api.coreMemories.utils.update);
const append = useMutation(api.coreMemories.utils.append);
const replace = useMutation(api.coreMemories.utils.replace);
const remove = useMutation(api.coreMemories.utils.remove);

const [persona, setPersona] = useState("");
const [human, setHuman] = useState("");
const [appendField, setAppendField] = useState<"persona" | "human">("persona");
const [appendText, setAppendText] = useState("");
const [replaceField, setReplaceField] = useState<"persona" | "human">("persona");
const [oldText, setOldText] = useState("");
const [newText, setNewText] = useState("");

useEffect(() => {
if (mem) {
setPersona(mem.persona ?? "");
setHuman(mem.human ?? "");
}
}, [mem]);

return (
<div className="flex flex-col gap-4">
<h2 className="text-lg font-semibold">Edit Memory</h2>
<div className="space-y-2">
<label className="text-sm font-medium">Persona</label>
<textarea className="w-full border rounded p-2" rows={5} value={persona} onChange={(e) => setPersona(e.target.value)} />
<button className="px-3 py-2 bg-blue-600 text-white rounded" onClick={() => void update({ persona })}>
Save Persona
</button>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Human</label>
<textarea className="w-full border rounded p-2" rows={5} value={human} onChange={(e) => setHuman(e.target.value)} />
<button className="px-3 py-2 bg-blue-600 text-white rounded" onClick={() => void update({ human })}>
Save Human
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<h3 className="font-medium">Append</h3>
<select className="border rounded p-2" value={appendField} onChange={(e) => setAppendField(e.target.value as any)}>
<option value="persona">persona</option>
<option value="human">human</option>
</select>
<input className="border rounded p-2 w-full" placeholder="Text to append" value={appendText} onChange={(e) => setAppendText(e.target.value)} />
<button className="px-3 py-2 bg-green-600 text-white rounded" onClick={() => void append({ field: appendField, text: appendText })}>
Append
</button>
</div>
<div className="space-y-2">
<h3 className="font-medium">Replace</h3>
<select className="border rounded p-2" value={replaceField} onChange={(e) => setReplaceField(e.target.value as any)}>
<option value="persona">persona</option>
<option value="human">human</option>
</select>
<input className="border rounded p-2 w-full" placeholder="Exact text to replace" value={oldText} onChange={(e) => setOldText(e.target.value)} />
<input className="border rounded p-2 w-full" placeholder="New text" value={newText} onChange={(e) => setNewText(e.target.value)} />
<button className="px-3 py-2 bg-orange-600 text-white rounded" onClick={() => void replace({ field: replaceField, oldContent: oldText, newContent: newText })}>
Replace
</button>
</div>
</div>
<button className="px-3 py-2 bg-red-600 text-white rounded w-min" onClick={() => void remove()}>
Delete Memory
</button>
</div>
);
}
13 changes: 13 additions & 0 deletions example/ui/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import FilesImages from "./files/FilesImages";
import RateLimiting from "./rate_limiting/RateLimiting";
import { WeatherFashion } from "./workflows/WeatherFashion";
import RagBasic from "./rag/RagBasic";
import MemoryUI from "./coreMemories/MemoryUI";

const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string);

Expand Down Expand Up @@ -40,6 +41,7 @@ export function App() {
<Route path="/rag-basic" element={<RagBasic />} />
<Route path="/rate-limiting" element={<RateLimiting />} />
<Route path="/weather-fashion" element={<WeatherFashion />} />
<Route path="/core-memories" element={<MemoryUI />} />
</Routes>
</main>
<Toaster />
Expand Down Expand Up @@ -69,6 +71,17 @@ function Index() {
enough to see it in action.
</p>
</li>
<li className="border rounded p-4 hover:shadow transition">
<Link
to="/core-memories"
className="text-xl font-semibold text-indigo-700 hover:underline"
>
Core Memories
</Link>
<p className="mt-2 text-gray-700">
Manage persona/human core memories with CRUD operations, append, and replace functions.
</p>
</li>
<li className="border rounded p-4 hover:shadow transition">
<Link
to="/chat-streaming"
Expand Down
Loading