Rember MCP

Official
import { AiChat } from "@effect/ai" import { AnthropicClient, AnthropicCompletions } from "@effect/ai-anthropic" import { FileSystem } from "@effect/platform" import { NodeContext, NodeHttpClient } from "@effect/platform-node" import { expect, it } from "@effect/vitest" import { Chunk, Config, DateTime, Effect, Layer, pipe, Schema, String } from "effect" import { ErrorReachedLimitUsageTracker, Rember } from "../rember.js" import { layerTools, ToolCreateFlashcards, toolkit } from "../tools.js" import { computeTextModel, logHistory, unrollToolCalls } from "./utils.js" // #: Layers // Set to true to run with the Claude.ai system prompt (it's more expensive but more realistic) const ENABLE_SYSTEM_PROMPT = true const layerCompletions = pipe( Effect.gen(function*() { const fs = yield* FileSystem.FileSystem // The system prompt can be found at https://docs.anthropic.com/en/release-notes/system-prompts#feb-24th-2025 const now = yield* DateTime.now const systemPromptClaude = yield* pipe( fs.readFileString("./src/test/system-prompt-claude-2025-02-24.md"), Effect.map(String.replace("{{currentDateTime}}", DateTime.formatIso(now))) ) return AnthropicCompletions.layerCompletions({ model: "claude-3-7-sonnet-20250219", config: { system: ENABLE_SYSTEM_PROMPT ? systemPromptClaude : undefined, max_tokens: 2000 } }) }), Layer.unwrapEffect ) const layerAnthropicClient = AnthropicClient.layerConfig({ apiKey: Config.redacted("ANTHROPIC_API_KEY") }) const layerRemberSucceed = Layer.succeed(Rember, { generateCardsAndCreateRembs: ({ notes }) => Effect.succeed({ usageMonth: notes.length, maxUsageMonth: 30, quantity: notes.length }) }) const layerRemberFailReachedLimitUsageTracker = Layer.succeed(Rember, { generateCardsAndCreateRembs: () => Effect.fail( new ErrorReachedLimitUsageTracker({ message: `Usage limit reached for feature 'generateCards': 30/30` }) ) }) const layerSucceed = pipe( Layer.mergeAll(layerCompletions, layerTools), Layer.provideMerge(layerRemberSucceed), Layer.provideMerge(layerAnthropicClient), Layer.provideMerge(NodeHttpClient.layerUndici), Layer.provideMerge(NodeContext.layer) ) const layerFailReachedLimitUsageTracker = pipe( Layer.mergeAll(layerCompletions, layerTools), Layer.provideMerge(layerRemberFailReachedLimitUsageTracker), Layer.provideMerge(layerAnthropicClient), Layer.provideMerge(NodeHttpClient.layerUndici), Layer.provideMerge(NodeContext.layer) ) // #: Postdam conference it.live("Postdam conference", ({ task }) => Effect.gen(function*() { const tools = yield* toolkit const chat = yield* AiChat.empty yield* chat.toolkit({ input: "Create a remb on the Postdam conference", tools }) yield* unrollToolCalls({ chat, tools, limit: 2 }) const history = yield* chat.history yield* logHistory({ chat, label: task.name }) // ##: Test model message 0 const messageModel0 = pipe( history, Chunk.filter((_) => _.role._tag === "Model"), Chunk.unsafeGet(0) ) const partsToolCallMessageModel0 = pipe( messageModel0.parts, Chunk.filter((_) => _._tag === "ToolCall"), Chunk.toArray ) expect(partsToolCallMessageModel0).toHaveLength(1) const inputToolCall0MessageModel0 = pipe( messageModel0.parts, Chunk.filter((_) => _._tag === "ToolCall"), Chunk.unsafeGet(0), (_) => ({ _tag: String.snakeToPascal(_.name), ...(_.params as any) }), Schema.decodeUnknownSync(ToolCreateFlashcards) ) expect(inputToolCall0MessageModel0.notes).toHaveLength(1) // ##: Test model message 1 const messageModel1 = pipe( history, Chunk.filter((_) => _.role._tag === "Model"), Chunk.unsafeGet(1) ) const partText0MessageModel1 = pipe( messageModel1.parts, Chunk.filter((_) => _._tag === "Text"), Chunk.unsafeGet(0) ) expect(partText0MessageModel1.content).toContain("1 remb") expect(partText0MessageModel1.content).toContain("https://rember.com/review") }).pipe( Effect.provide(layerSucceed) )) // #: Deficit vs Debt it.live("Deficit vs Debt", ({ task }) => Effect.gen(function*() { const tools = yield* toolkit const chat = yield* AiChat.empty yield* chat.toolkit({ input: "What's the definition of deficit? What's the difference with debt?", tools }) yield* chat.toolkit({ input: "Help me remember this", tools }) yield* unrollToolCalls({ chat, tools, limit: 2 }) const history = yield* chat.history yield* logHistory({ chat, label: task.name }) // ##: Test model message 0 const messageModel0 = pipe( history, Chunk.filter((_) => _.role._tag === "Model"), Chunk.unsafeGet(0) ) const partsToolCallMessageModel0 = pipe( messageModel0.parts, Chunk.filter((_) => _._tag === "ToolCall"), Chunk.toArray ) expect(partsToolCallMessageModel0).toHaveLength(0) // ##: Test model message 1 const messageModel1 = pipe( history, Chunk.filter((_) => _.role._tag === "Model"), Chunk.unsafeGet(1) ) const partsToolCallMessageModel1 = pipe( messageModel1.parts, Chunk.filter((_) => _._tag === "ToolCall"), Chunk.toArray ) expect(partsToolCallMessageModel1).toHaveLength(1) const inputToolCall0MessageModel1 = pipe( messageModel1.parts, Chunk.filter((_) => _._tag === "ToolCall"), Chunk.unsafeGet(0), (_) => ({ _tag: String.snakeToPascal(_.name), ...(_.params as any) }), Schema.decodeUnknownSync(ToolCreateFlashcards) ) expect(inputToolCall0MessageModel1.notes).toHaveLength(1) }).pipe( Effect.provide(layerSucceed) )) // #: Thesis chapter 2 it.live("Thesis chapter 2", ({ task }) => Effect.gen(function*() { const fs = yield* FileSystem.FileSystem const tools = yield* toolkit const chat = yield* AiChat.empty const thesisChapter2 = yield* fs.readFileString("./src/test/thesis-chapter-2.md") yield* chat.toolkit({ input: [ ["<document>", thesisChapter2, "</document>"].join("\n"), pipe( ` |I've attached chapter 2 from "Memory Models for Spaced Repetition Systems" by Giacomo Randazzo | |I'm only interested in: |- How DASH differs from IRT at a high level |- The formula for Duolingo's model |- Why is SM-17 important | |Add to Rember `, String.stripMargin, String.trim ) ].join("\n\n---\n\n"), tools }) yield* unrollToolCalls({ chat, tools, limit: 2 }) const history = yield* chat.history yield* logHistory({ chat, label: task.name }) // ##: Test model message 0 const messageModel0 = pipe( history, Chunk.filter((_) => _.role._tag === "Model"), Chunk.unsafeGet(0) ) const partsToolCallMessageModel0 = pipe( messageModel0.parts, Chunk.filter((_) => _._tag === "ToolCall"), Chunk.toArray ) expect(partsToolCallMessageModel0).toHaveLength(1) const inputToolCall0MessageModel0 = pipe( messageModel0.parts, Chunk.filter((_) => _._tag === "ToolCall"), Chunk.unsafeGet(0), (_) => ({ _tag: String.snakeToPascal(_.name), ...(_.params as any) }), Schema.decodeUnknownSync(ToolCreateFlashcards) ) expect(inputToolCall0MessageModel0.notes).toHaveLength(3) expect(inputToolCall0MessageModel0.source).toBeDefined() expect(inputToolCall0MessageModel0.source).toContain("Memory Models for Spaced Repetition Systems") }).pipe( Effect.provide(layerSucceed) )) // #: Fail with usage tracker limit reached it.live("Fail with usage tracker limit reached", ({ task }) => Effect.gen(function*() { const tools = yield* toolkit const chat = yield* AiChat.empty yield* chat.toolkit({ input: "Create a few flashcards on the postdam conference", tools }) yield* unrollToolCalls({ chat, tools, limit: 2 }) const history = yield* chat.history yield* logHistory({ chat, label: task.name }) // ##: Test model message 1 const messageModel1 = pipe( history, Chunk.filter((_) => _.role._tag === "Model"), Chunk.unsafeGet(1) ) const partText0MessageModel1 = pipe( messageModel1.parts, Chunk.filter((_) => _._tag === "Text"), Chunk.unsafeGet(0) ) expect(partText0MessageModel1.content).toContain("Rember Pro") expect(partText0MessageModel1.content).toContain("https://rember.com/settings/account") }).pipe( Effect.provide(layerFailReachedLimitUsageTracker) )) // #: How to use Rember it.live("How to use Rember", ({ task }) => Effect.gen(function*() { const tools = yield* toolkit const chat = yield* AiChat.empty yield* chat.toolkit({ input: "How can I use the MCP for Rember?", tools }) yield* chat.toolkit({ input: "How and where should I access these flashcards?", tools }) const history = yield* chat.history yield* logHistory({ chat, label: task.name }) // ##: Test model messages const messagesWithToolCalls0 = pipe( history, Chunk.filter((_) => _.role._tag === "Model" && Chunk.some(_.parts, (_) => _._tag === "ToolCall")) ) expect(messagesWithToolCalls0).toHaveLength(0) const textModel = yield* computeTextModel({ chat }) expect(textModel).not.toContain("Rember API") expect(textModel).toContain("https://rember.com") }).pipe( Effect.provide(layerFailReachedLimitUsageTracker) ))
ID: e13i6926nu