diff --git a/typescript/m1/m1.2_scratch_agent.ts b/typescript/m1/m1.2_scratch_agent.ts index 3ea9057..68c56f9 100644 --- a/typescript/m1/m1.2_scratch_agent.ts +++ b/typescript/m1/m1.2_scratch_agent.ts @@ -4,6 +4,8 @@ import { model } from "../models.js"; const agent = createDeepAgent({ model }); -const result = await agent.invoke({"messages": [{"role": "user", "content": "What is an LLM?"}]}); +const result = await agent.invoke({ + messages: [{ role: "user", content: "What is an LLM?" }] +}); -console.log(result.messages[result.messages.length - 1].content); +console.log(result.messages.at(-1)?.content); diff --git a/typescript/m1/m1.4_scratch_agent_butler.ts b/typescript/m1/m1.4_scratch_agent_butler.ts index 69f019f..1d7c018 100644 --- a/typescript/m1/m1.4_scratch_agent_butler.ts +++ b/typescript/m1/m1.4_scratch_agent_butler.ts @@ -1,13 +1,14 @@ +import { context } from "langchain"; import { createDeepAgent } from "deepagents"; import { model } from "../models.js"; -const SYSTEM_PROMPT = - "YOU ARE AN EXTREMELY POSH BRITISH BUTLER. You speak ONLY in the most " + - "refined, formal, over-the-top Victorian English. You say 'indeed', 'quite', " + - "'I dare say', 'one simply must' constantly. You find all things common or " + - "nautical to be utterly beneath you. You NEVER break character under ANY " + - "circumstances."; +const SYSTEM_PROMPT = context` + YOU ARE AN EXTREMELY POSH BRITISH BUTLER. You speak ONLY in the most + refined, formal, over-the-top Victorian English. You say 'indeed', 'quite', + 'I dare say', 'one simply must' constantly. You find all things common or + nautical to be utterly beneath you. You NEVER break character under ANY + circumstances.`; const agent = createDeepAgent({ model, @@ -19,4 +20,4 @@ const result = await agent.invoke({ messages: [{ role: "user", content: "What is an LLM?" }], }); -console.log(result.messages[result.messages.length - 1].content); +console.log(result.messages.at(-1)?.content); diff --git a/typescript/m1/m1.5_scratch_agent_tools.ts b/typescript/m1/m1.5_scratch_agent_tools.ts index e682737..c2abd57 100644 --- a/typescript/m1/m1.5_scratch_agent_tools.ts +++ b/typescript/m1/m1.5_scratch_agent_tools.ts @@ -1,8 +1,9 @@ import { DatabaseSync } from "node:sqlite"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; -import { tool } from "@langchain/core/tools"; + import { z } from "zod"; +import { tool } from "langchain"; import { createDeepAgent } from "deepagents"; import { model } from "../models.js"; @@ -47,4 +48,4 @@ const result = await agent.invoke({ messages: [{ role: "user", content: "Which five genres have the most tracks?" }], }); -console.log(result.messages[result.messages.length - 1].content); +console.log(result.messages.at(-1)?.content); diff --git a/typescript/m1/m1.6_agent_mcp.ts b/typescript/m1/m1.6_agent_mcp.ts index e6fe940..982b8b6 100644 --- a/typescript/m1/m1.6_agent_mcp.ts +++ b/typescript/m1/m1.6_agent_mcp.ts @@ -12,28 +12,30 @@ const client = new MultiServerMCPClient({ }, }); -let tools = await client.getTools(); - -console.log(`\ndocs-langchain: ${tools.length} tool(s)`); -for (const t of tools) { - console.log(` ${t.name}`); - console.log(` ${t.description?.slice(0, 90)}`); -} - -tools = tools.filter((t) => ALLOWED.has(t.name)); - -const agent = createDeepAgent({ model, tools }); - -const result = await agent.invoke({ - messages: [ - { - role: "user", - content: - "Use the LangChain docs MCP tool to explain what MCP is and how LangChain uses MCP tools.", - }, - ], -}); - -console.log(result.messages[result.messages.length - 1].content); - -await client.close(); +try { + let tools = await client.getTools(); + + console.log(`\ndocs-langchain: ${tools.length} tool(s)`); + for (const t of tools) { + console.log(` ${t.name}`); + console.log(` ${t.description?.slice(0, 90)}`); + } + + tools = tools.filter((t) => ALLOWED.has(t.name)); + + const agent = createDeepAgent({ model, tools }); + + const result = await agent.invoke({ + messages: [ + { + role: "user", + content: + "Use the LangChain docs MCP tool to explain what MCP is and how LangChain uses MCP tools.", + }, + ], + }); + + console.log(result.messages.at(-1)?.content); +} finally { + await client.close(); +} \ No newline at end of file diff --git a/typescript/m1/m1.7_messages_threads_checkpointers.ts b/typescript/m1/m1.7_messages_threads_checkpointers.ts index 424cb43..ea2fd72 100644 --- a/typescript/m1/m1.7_messages_threads_checkpointers.ts +++ b/typescript/m1/m1.7_messages_threads_checkpointers.ts @@ -16,18 +16,18 @@ let result = await agent.invoke( threadA ); console.log("Thread A, turn 1:"); -console.log(result.messages[result.messages.length - 1].content); +console.log(result.messages.at(-1)?.content); result = await agent.invoke( { messages: [{ role: "user", content: "What is my favorite color?" }] }, threadA ); console.log("\nThread A, turn 2:"); -console.log(result.messages[result.messages.length - 1].content); +console.log(result.messages.at(-1)?.content); result = await agent.invoke( { messages: [{ role: "user", content: "What is my favorite color?" }] }, threadB ); console.log("\nThread B, turn 1:"); -console.log(result.messages[result.messages.length - 1].content); +console.log(result.messages.at(-1)?.content); diff --git a/typescript/m1/m1.8_hitl.ts b/typescript/m1/m1.8_hitl.ts index fbbef45..46d11fc 100644 --- a/typescript/m1/m1.8_hitl.ts +++ b/typescript/m1/m1.8_hitl.ts @@ -1,9 +1,8 @@ import { createInterface } from "node:readline/promises"; -import { tool } from "@langchain/core/tools"; -import { isToolMessage } from "@langchain/core/messages"; import { z } from "zod"; -import { MemorySaver } from "@langchain/langgraph"; -import { Command, INTERRUPT, isInterrupted } from "@langchain/langgraph"; + +import { tool, ToolMessage } from "langchain"; +import { Command, INTERRUPT, isInterrupted, MemorySaver } from "@langchain/langgraph"; import { createDeepAgent } from "deepagents"; import { model } from "../models.js"; @@ -97,7 +96,7 @@ while (isInterrupted(result) && result[INTERRUPT].length) { rl.close(); for (const msg of result.messages) { - if (isToolMessage(msg) && msg.name === "send_email") { + if (ToolMessage.isInstance(msg) && msg.name === "send_email") { console.log(msg.content); break; } diff --git a/typescript/m2/m2.2_agent.ts b/typescript/m2/m2.2_agent.ts index 72bf914..26f9fba 100644 --- a/typescript/m2/m2.2_agent.ts +++ b/typescript/m2/m2.2_agent.ts @@ -3,6 +3,7 @@ import { dirname, join } from "node:path"; import { mkdirSync, writeFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; +import { context } from "langchain"; import { CompositeBackend, FilesystemBackend, @@ -59,15 +60,15 @@ try { messages: [ { role: "user", - content: - "Read /reference/chinook-sales.md, then add this note to it: " + - "'Current promotion: 20% off all Jazz albums through end of month.'", + content: context` + Read /reference/chinook-sales.md, then add this note to it: + 'Current promotion: 20% off all Jazz albums through end of month.'`, }, ], }, { configurable: { thread_id: "lab-m2.2" } } ); - console.log(result.messages[result.messages.length - 1].content); + console.log(result.messages.at(-1)?.content); } catch (e) { const err = e instanceof Error ? e : new Error(String(e)); const cause = err.cause instanceof Error ? err.cause : undefined; diff --git a/typescript/m2/m2.3_sales_agent.ts b/typescript/m2/m2.3_sales_agent.ts index 0f6b304..cb502ec 100644 --- a/typescript/m2/m2.3_sales_agent.ts +++ b/typescript/m2/m2.3_sales_agent.ts @@ -5,6 +5,7 @@ import { writeFileSync } from "node:fs"; import { readFile } from "node:fs/promises"; import { fileURLToPath } from "node:url"; +import { context } from "langchain"; import { LangSmithSandbox, createDeepAgent } from "deepagents"; import { SandboxClient } from "langsmith/sandbox"; @@ -34,12 +35,12 @@ for (const result of uploadResults) { const agent = createDeepAgent({ model, backend, - systemPrompt: - "You are a sales data analyst with access to the Chinook music store database " + - "at /chinook.db. Use sqlite3 and matplotlib to answer questions with charts. " + - "Install any packages you need with pip before importing them. " + - "When asked to produce a chart, write a Python script, execute it, and confirm " + - "the output file was created.", + systemPrompt: context` + You are a sales data analyst with access to the Chinook music store database + at /chinook.db. Use sqlite3 and matplotlib to answer questions with charts. + Install any packages you need with pip before importing them. + When asked to produce a chart, write a Python script, execute it, and confirm + the output file was created.`, }); try { @@ -47,20 +48,20 @@ try { messages: [ { role: "user", - content: - "Query the Chinook database at /chinook.db to get total revenue " + - "by genre. Create a clean donut chart showing each genre's share " + - "of total sales revenue. Group any genres that individually " + - "account for less than 3% of total revenue into a single 'Other' " + - "slice. Label each slice with the genre name and percentage. " + - "Use a visually distinct color palette, leave a white center hole, " + - "and make sure no labels overlap with each other or with the title. " + - "Add enough top padding so the title is fully visible. " + - "Save the chart to /genre_revenue.png.", + content: context` + Query the Chinook database at /chinook.db to get total revenue + by genre. Create a clean donut chart showing each genre's share + of total sales revenue. Group any genres that individually + account for less than 3% of total revenue into a single 'Other' + slice. Label each slice with the genre name and percentage. + Use a visually distinct color palette, leave a white center hole, + and make sure no labels overlap with each other or with the title. + Add enough top padding so the title is fully visible. + Save the chart to /genre_revenue.png.`, }, ], }); - console.log(result.messages[result.messages.length - 1].content); + console.log(result.messages.at(-1)?.content); const pngBytes = await ls_sandbox.read("/genre_revenue.png"); const outPath = join(__dirname, "genre_revenue.png"); diff --git a/typescript/m2/m2.3_sandbox_agent.ts b/typescript/m2/m2.3_sandbox_agent.ts index ed79056..1250450 100644 --- a/typescript/m2/m2.3_sandbox_agent.ts +++ b/typescript/m2/m2.3_sandbox_agent.ts @@ -1,6 +1,7 @@ // typescript/m2/m2.3_sandbox_agent.ts import { randomUUID } from "node:crypto"; +import { context } from "langchain"; import { LangSmithSandbox, createDeepAgent } from "deepagents"; import { SandboxClient } from "langsmith/sandbox"; @@ -16,9 +17,9 @@ const backend = new LangSmithSandbox({ sandbox: ls_sandbox }); const agent = createDeepAgent({ model, backend, - systemPrompt: - "You are a coding assistant. When asked to run code, write the script " + - "to a file first, then execute it. Show the output in your final answer.", + systemPrompt: context` + You are a coding assistant. When asked to run code, write the script + to a file first, then execute it. Show the output in your final answer.`, }); try { @@ -26,13 +27,13 @@ try { messages: [ { role: "user", - content: - "Write a Python script that prints the first 15 Fibonacci numbers, " + - "save it to fib.py, and run it.", + content: context` + Write a Python script that prints the first 15 Fibonacci numbers, + save it to fib.py, and run it.`, }, ], }); - console.log(result.messages[result.messages.length - 1].content); + console.log(result.messages.at(-1)?.content); } finally { await client.deleteSandbox(ls_sandbox.name); } diff --git a/typescript/m2/m2.4_eval_agent.ts b/typescript/m2/m2.4_eval_agent.ts index 77fbbc4..1ba3e10 100644 --- a/typescript/m2/m2.4_eval_agent.ts +++ b/typescript/m2/m2.4_eval_agent.ts @@ -18,4 +18,4 @@ const result = await agent.invoke({ ], }); -console.log(result.messages[result.messages.length - 1].content); +console.log(result.messages.at(-1)?.content); diff --git a/typescript/m2/m2.4_interpreter_agent.ts b/typescript/m2/m2.4_interpreter_agent.ts index c44cca6..8077d58 100644 --- a/typescript/m2/m2.4_interpreter_agent.ts +++ b/typescript/m2/m2.4_interpreter_agent.ts @@ -4,29 +4,29 @@ import { DatabaseSync } from "node:sqlite"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; -import { createCodeInterpreterMiddleware } from "@langchain/quickjs"; -import { tool } from "@langchain/core/tools"; -import { createDeepAgent } from "deepagents"; import { z } from "zod"; +import { context, tool } from "langchain"; +import { createDeepAgent } from "deepagents"; +import { createCodeInterpreterMiddleware } from "@langchain/quickjs"; import { model } from "../models.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); const DB_PATH = join(__dirname, "chinook.db"); -const TASK = - "Who is our top-selling artist, what is their best-selling album, " + - "what is the most-purchased track on that album, " + - "and how many distinct customers have bought that track? " + - "Each answer depends on the result of the previous query."; +const TASK = context` + Who is our top-selling artist, what is their best-selling album, + what is the most-purchased track on that album, + and how many distinct customers have bought that track? + Each answer depends on the result of the previous query.`; -const SYSTEM = - "You are a sales analyst for Chinook Digital Music Store. " + - "Use the query_chinook tool to query the database. " + - "Key tables: Artist(ArtistId, Name), Album(AlbumId, Title, ArtistId), " + - "Track(TrackId, Name, AlbumId), " + - "InvoiceLine(InvoiceLineId, InvoiceId, TrackId, UnitPrice, Quantity). " + - "Revenue is InvoiceLine.UnitPrice * InvoiceLine.Quantity."; +const SYSTEM = context` + You are a sales analyst for Chinook Digital Music Store. + Use the query_chinook tool to query the database. + Key tables: Artist(ArtistId, Name), Album(AlbumId, Title, ArtistId), + Track(TrackId, Name, AlbumId), + InvoiceLine(InvoiceLineId, InvoiceId, TrackId, UnitPrice, Quantity). + Revenue is InvoiceLine.UnitPrice * InvoiceLine.Quantity.`; const queryChinook = tool( ({ sql }) => { @@ -53,13 +53,13 @@ const agentWith = createDeepAgent({ model, tools: [queryChinook], middleware: [createCodeInterpreterMiddleware({ ptc: ["query_chinook"] })], - systemPrompt: - SYSTEM + - " The eval tool supports Programmatic Tool Calling (PTC): JavaScript" + - " running inside eval() can call query_chinook via tools.queryChinook()." + - " For dependent queries where each answer requires a result from the" + - " previous, prefer a single eval() call that chains all queries in" + - " JavaScript — intermediate values stay in variables and never return to the model.", + systemPrompt: context` + ${SYSTEM} + The eval tool supports Programmatic Tool Calling (PTC): JavaScript + running inside eval() can call query_chinook via tools.queryChinook(). + For dependent queries where each answer requires a result from the + previous, prefer a single eval() call that chains all queries in + JavaScript — intermediate values stay in variables and never return to the model.`, }); const resultWith = await agentWith.invoke( diff --git a/typescript/m3/m3.1_summarization.ts b/typescript/m3/m3.1_summarization.ts index 37e0bb1..8dd6402 100644 --- a/typescript/m3/m3.1_summarization.ts +++ b/typescript/m3/m3.1_summarization.ts @@ -12,8 +12,8 @@ // npx tsx m3/m3.1_summarization.ts import { createDeepAgent } from "deepagents"; +import { HumanMessage } from "langchain"; import { MemorySaver } from "@langchain/langgraph"; -import { HumanMessage } from "@langchain/core/messages"; import { model } from "../models.js"; @@ -47,7 +47,7 @@ async function turn(message: string): Promise { { messages: [new HumanMessage(message)] }, THREAD ); - return result.messages[result.messages.length - 1].content; + return result.messages.at(-1)?.content; } async function showState(): Promise { diff --git a/typescript/m3/m3.2_scratch_agent_skills.ts b/typescript/m3/m3.2_scratch_agent_skills.ts index 5a08897..e06795c 100644 --- a/typescript/m3/m3.2_scratch_agent_skills.ts +++ b/typescript/m3/m3.2_scratch_agent_skills.ts @@ -2,6 +2,7 @@ import { dirname } from "node:path"; import { fileURLToPath } from "node:url"; +import { context } from "langchain"; import { createDeepAgent, FilesystemBackend } from "deepagents"; import { model } from "../models.js"; @@ -21,13 +22,13 @@ const result = await agent.invoke({ messages: [ { role: "user", - content: - "Qualify this lead: Acme Corp, 200-person logistics company. I spoke with " + - "Sarah Chen, VP of Sales: she's the decision maker. They have $45k budgeted " + - "for CRM this year. Main pain: deals are slipping through the cracks due to " + - "poor pipeline visibility. They want a solution live by end of Q3.", + content: context` + Qualify this lead: Acme Corp, 200-person logistics company. I spoke with + Sarah Chen, VP of Sales: she's the decision maker. They have $45k budgeted + for CRM this year. Main pain: deals are slipping through the cracks due to + poor pipeline visibility. They want a solution live by end of Q3.`, }, ], }); -console.log(result.messages[result.messages.length - 1].content); +console.log(result.messages.at(-1)?.content); diff --git a/typescript/m3/m3.3_memory_agent.ts b/typescript/m3/m3.3_memory_agent.ts index 8fa8c43..e89fbf1 100644 --- a/typescript/m3/m3.3_memory_agent.ts +++ b/typescript/m3/m3.3_memory_agent.ts @@ -78,7 +78,7 @@ const result = await agent.invoke( demoConfig ); console.log("--- Question 1 ---"); -console.log(result.messages[result.messages.length - 1].content); +console.log(result.messages.at(-1)?.content); // Second invoke: agent writes to memory const result2 = await agent.invoke( diff --git a/typescript/m4/m4.2_run_newsletter.ts b/typescript/m4/m4.2_run_newsletter.ts index b6fcdd8..8e97332 100644 --- a/typescript/m4/m4.2_run_newsletter.ts +++ b/typescript/m4/m4.2_run_newsletter.ts @@ -29,7 +29,7 @@ const result = await agent.invoke( ); // The editor's final reply (coordination summary). -console.log(result.messages[result.messages.length - 1].content); +console.log(result.messages.at(-1)?.content); // Everything the agent produced lives in agent state (not on your disk — it ran // in the default StateBackend). Pull it all out and mirror it to OUT_DIR: the diff --git a/typescript/m4/m4_2_newsletter_agent.ts b/typescript/m4/m4_2_newsletter_agent.ts index 0c3a690..fd8f0a3 100644 --- a/typescript/m4/m4_2_newsletter_agent.ts +++ b/typescript/m4/m4_2_newsletter_agent.ts @@ -13,16 +13,17 @@ * and writes it to disk. */ +import { z } from "zod"; import { marked } from "marked"; import sanitizeHtml from "sanitize-html"; -import { TavilySearchAPIWrapper } from "@langchain/tavily"; -import { tool } from "@langchain/core/tools"; -import { z } from "zod"; + +import { context, tool } from "langchain"; import { createDeepAgent, type FilesystemPermission, type SubAgent, } from "deepagents"; +import { TavilySearchAPIWrapper } from "@langchain/tavily"; import { model, strongModel } from "../models.js"; @@ -35,7 +36,7 @@ const tavilyApiKey = process.env.TAVILY_API_KEY; if (!tavilyApiKey) { throw new Error( "TAVILY_API_KEY is required for the Module 4.2 newsletter lab. " + - "Set it in your environment before running m4.2_run_newsletter.ts." + "Set it in your environment before running m4.2_run_newsletter.ts." ); } @@ -47,9 +48,9 @@ const internetSearch = tool( }, { name: "internet_search", - description: - "Search the web for recent news. Use this to research what's new in a " + - "music genre — new releases, notable artists, trends, and events.", + description: context` + Search the web for recent news. Use this to research what's new in a + music genre — new releases, notable artists, trends, and events.`, schema: z.object({ query: z.string(), maxResults: z.number().default(8), @@ -92,9 +93,9 @@ const markdownToHtml = tool( }, { name: "markdown_to_html", - description: - "Convert a Markdown newsletter into a complete, styled HTML page. " + - "Returns the full HTML document as a string.", + description: context` + Convert a Markdown newsletter into a complete, styled HTML page. + Returns the full HTML document as a string.`, schema: z.object({ markdownText: z.string(), title: z.string().default("This Week in Music"), @@ -103,27 +104,28 @@ const markdownToHtml = tool( ); // --- The research subagent ------------------------------------------------- -const GENRE_PROMPT = `You are a music journalist researching one genre for an -online music distributor's weekly newsletter. - -You will be given a single genre and an assigned research folder to work in. - -How to work: -1. Use internet_search to find recent, noteworthy developments in that genre - — new releases, notable artists, trends, or events. Run a few searches. -2. Save the COMPLETE, verbatim output of ALL your searches to a single file: - write_file("/research//sources.md", ...). Paste the results exactly - as the tool returned them — every result's title, URL, and full content - snippet. Do NOT summarize, trim, or reformat. This one file is your raw - archive: all the bulky material stays here so it never clutters the - editor's context. -3. Only then, from what you found, write one tight newsletter segment. - -Return ONLY the finished segment as your reply: -- A markdown section: a "## " heading followed by ~120-180 words. -- Lively but factual newsletter tone; name specific artists and releases. -- Do NOT paste raw search results or link dumps into your reply — those live - in your research files. Return just the polished segment.`; +const GENRE_PROMPT = context` + You are a music journalist researching one genre for an + online music distributor's weekly newsletter. + + You will be given a single genre and an assigned research folder to work in. + + How to work: + 1. Use internet_search to find recent, noteworthy developments in that genre + — new releases, notable artists, trends, or events. Run a few searches. + 2. Save the COMPLETE, verbatim output of ALL your searches to a single file: + write_file("/research//sources.md", ...). Paste the results exactly + as the tool returned them — every result's title, URL, and full content + snippet. Do NOT summarize, trim, or reformat. This one file is your raw + archive: all the bulky material stays here so it never clutters the + editor's context. + 3. Only then, from what you found, write one tight newsletter segment. + + Return ONLY the finished segment as your reply: + - A markdown section: a "## " heading followed by ~120-180 words. + - Lively but factual newsletter tone; name specific artists and releases. + - Do NOT paste raw search results or link dumps into your reply — those live + in your research files. Return just the polished segment.`; // Researchers may write under /research/** and are denied writes elsewhere. const researchPermissions: FilesystemPermission[] = [ @@ -133,9 +135,9 @@ const researchPermissions: FilesystemPermission[] = [ const genreResearcher: SubAgent = { name: "genre-researcher", - description: - "Research one music genre and write a short newsletter segment about " + - "what's new in it. Delegate one genre per call.", + description: context` + Research one music genre and write a short newsletter segment about + what's new in it. Delegate one genre per call.`, systemPrompt: GENRE_PROMPT, // its own brain — never inherited tools: [internetSearch], // override — replaces the inherited set model, // override — the cheaper Haiku 4.5 @@ -143,21 +145,22 @@ const genreResearcher: SubAgent = { }; // --- The editor (main agent) ----------------------------------------------- -const EDITOR_PROMPT = `You are the editor of an online music distributor's weekly -newsletter. This week you are featuring the distributor's top genres: -${TOP_GENRES.join(", ")}. - -Your job: -1. For EACH genre, delegate to the genre-researcher subagent using the task - tool — fire them off in parallel. Tell each one which genre to cover and - which assigned folder to use (/research//), and ask for one segment. -2. Collect the four returned segments. Do NOT research genres yourself. -3. Assemble them into one Markdown newsletter: a top-level "# This Week in - Music" title, a one-sentence intro, then the four "## " sections. -4. Call markdown_to_html on the assembled Markdown, then write_file the - returned HTML to /output/newsletter.html. - -Keep your own context focused on coordination and assembly.`; +const EDITOR_PROMPT = context` + You are the editor of an online music distributor's weekly + newsletter. This week you are featuring the distributor's top genres: + ${TOP_GENRES.join(", ")}. + + Your job: + 1. For EACH genre, delegate to the genre-researcher subagent using the task + tool — fire them off in parallel. Tell each one which genre to cover and + which assigned folder to use (/research//), and ask for one segment. + 2. Collect the four returned segments. Do NOT research genres yourself. + 3. Assemble them into one Markdown newsletter: a top-level "# This Week in + Music" title, a one-sentence intro, then the four "## " sections. + 4. Call markdown_to_html on the assembled Markdown, then write_file the + returned HTML to /output/newsletter.html. + + Keep your own context focused on coordination and assembly.`; // The editor uses the stronger shared model; the researcher overrides to the // cheaper `model` (Haiku 4.5) above. Both come from models.ts. diff --git a/typescript/m5/sales_assistant/agent.ts b/typescript/m5/sales_assistant/agent.ts index 1c97cd0..a1cfc64 100644 --- a/typescript/m5/sales_assistant/agent.ts +++ b/typescript/m5/sales_assistant/agent.ts @@ -13,6 +13,7 @@ import { fileURLToPath } from "node:url"; import { MultiServerMCPClient } from "@langchain/mcp-adapters"; import { createCodeInterpreterMiddleware } from "@langchain/quickjs"; +import { context } from "langchain"; import { FilesystemBackend, createDeepAgent } from "deepagents"; import { strongModel } from "../../models.js"; @@ -22,10 +23,10 @@ import { markdownToHtml } from "./tools/html.js"; const HERE = dirname(fileURLToPath(import.meta.url)); -const SYSTEM_PROMPT = - "You are a sales assistant for Jane Peacock, a Sales Support Agent at " + - "Chinook, an online music distributor. Follow your operating manual (loaded " + - "from your memory) and use the matching playbook from /skills/ for each task."; +const SYSTEM_PROMPT = context` + You are a sales assistant for Jane Peacock, a Sales Support Agent at + Chinook, an online music distributor. Follow your operating manual (loaded + from your memory) and use the matching playbook from /skills/ for each task.`; const enableSearch = Boolean(process.env.TAVILY_API_KEY); if (!enableSearch) { diff --git a/typescript/m5/sales_assistant/mcp/mock_mail_server.ts b/typescript/m5/sales_assistant/mcp/mock_mail_server.ts index f9a21d3..b1d27f9 100644 --- a/typescript/m5/sales_assistant/mcp/mock_mail_server.ts +++ b/typescript/m5/sales_assistant/mcp/mock_mail_server.ts @@ -16,6 +16,7 @@ import { createServer } from "node:http"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { z } from "zod"; +import { context } from "langchain"; import { loadStore, nextId, saveStore } from "./mail_store.js"; @@ -29,12 +30,12 @@ function buildServer(): McpServer { mcp.registerTool( "mail_list_messages", { - description: - "List messages in the inbox. Returns a summary (id, from, subject, " + - "date, snippet) for each message — not the full body. Use " + - "mail_read_message to open one. The optional query is a " + - "case-insensitive substring matched against the subject and sender, " + - "mostly to mirror Gmail's search box; leave it empty to list everything.", + description: context` + List messages in the inbox. Returns a summary (id, from, subject, + date, snippet) for each message — not the full body. Use + mail_read_message to open one. The optional query is a + case-insensitive substring matched against the subject and sender, + mostly to mirror Gmail's search box; leave it empty to list everything.`, inputSchema: { query: z.string().default("") }, }, async ({ query }) => { @@ -70,11 +71,11 @@ function buildServer(): McpServer { mcp.registerTool( "mail_create_draft", { - description: - "Save a reply to the drafts folder. Does NOT send. Mirrors a real " + - "Gmail \"create draft\" call: the message is staged for the human to " + - "review and send later. In this course a human-in-the-loop gate runs " + - "before this tool, so a draft is only written after explicit approval.", + description: context` + Save a reply to the drafts folder. Does NOT send. Mirrors a real + Gmail "create draft" call: the message is staged for the human to + review and send later. In this course a human-in-the-loop gate runs + before this tool, so a draft is only written after explicit approval.`, inputSchema: { to: z.string(), subject: z.string(), body: z.string() }, }, async ({ to, subject, body }) => { diff --git a/typescript/m5/sales_assistant/subagents.ts b/typescript/m5/sales_assistant/subagents.ts index 14b2a53..77c3090 100644 --- a/typescript/m5/sales_assistant/subagents.ts +++ b/typescript/m5/sales_assistant/subagents.ts @@ -21,14 +21,17 @@ * `mail_create_draft` and `add_customer` solely on gated specialists means the * only path to either write runs through its human-approval gate. */ -import type { StructuredTool } from "@langchain/core/tools"; import { type AnyBackendProtocol, type FilesystemPermission, type SubAgent, createMemoryMiddleware, } from "deepagents"; -import type { InterruptOnConfig } from "langchain"; +import { + context, + type InterruptOnConfig, + type StructuredTool, +} from "langchain"; import { model, strongModel } from "../../models.js"; import { internetSearch } from "./tools/search.js"; @@ -39,65 +42,69 @@ const APPROVE_EDIT_REJECT: InterruptOnConfig = { allowedDecisions: ["approve", "edit", "reject"], }; -const ANALYST_PROMPT = `You are the chinook-analyst, the data specialist for the \ -Chinook Sales Assistant. You are the only agent that touches the database. - -Detailed operating instructions and the database schema live in your memory \ -(loaded automatically). Follow them. In short: answer with exact figures from \ -\`query_chinook\`, learn the schema once with \`introspect_schema\` and record it \ -in your memory, and use \`add_customer\` only when asked to add a genuinely new \ -customer (a human approves that write).`; - -const INBOX_PROMPT = `You are the inbox-manager, the email specialist for the \ -Chinook Sales Assistant. You own Jane's inbox and are the only agent that \ -touches it. - -Your tools (MCP, prefixed with the server name "mail"): -- \`mail_list_messages\` — list inbox messages (optionally filtered by a query). -- \`mail_read_message\` — read one message in full by id. -- \`mail_create_draft\` — save a reply to the drafts folder. It NEVER sends. - -When asked to find or read mail, return a tight summary the caller can act on \ -(sender, subject, and the key content) — not the raw dump. - -When asked to save a draft, just call \`mail_create_draft\` with the given \ -recipient, subject, and body. Saving a draft pauses automatically for Jane to \ -approve, edit, or reject — that pause IS the approval, so don't ask for \ -permission in prose first; make the call. Never invent a send tool; you only \ -ever create drafts.`; - -const REVIEWER_PROMPT = `You are the quote-reviewer. You receive a drafted quote — \ -line items (description, quantity, unit price, line total), any discount, and \ -the grand total — and you check it before it goes to the customer. - -Verify: -- The arithmetic: quantity x unit price for each line, and the grand total. -- Internal consistency: any stated discount is actually applied; nothing is \ -double-counted or missing. -- Plausibility: unit prices look like catalogue prices (tracks are normally \ -about $0.99); totals aren't off by an order of magnitude. - -Reply concisely: either "Looks correct" with a one-line confirmation, or a \ -short list of specific corrections. Do not rewrite the customer email — just \ -review the numbers and terms.`; - -const GENRE_PROMPT = `You are a music journalist researching one genre for an \ -online music distributor's weekly newsletter. - -You will be given a single genre and a private research folder to work in. - -How to work: -1. Use internet_search to find recent, noteworthy developments in that genre \ - — new releases, notable artists, trends, or events. Run a few searches. -2. Save the COMPLETE, verbatim output of ALL your searches to a single file: \ - write_file("/research//sources.md", ...). Do NOT summarize or trim. \ - This keeps the bulky material out of the editor's context. -3. Only then, from what you found, write one tight newsletter segment. - -Return ONLY the finished segment as your reply: -- A markdown section: a "## " heading followed by ~120-180 words. -- Lively but factual; name specific artists and releases. -- Do NOT paste raw search results into your reply — those live in your files.`; +const ANALYST_PROMPT = context` + You are the chinook-analyst, the data specialist for the + Chinook Sales Assistant. You are the only agent that touches the database. + + Detailed operating instructions and the database schema live in your memory + (loaded automatically). Follow them. In short: answer with exact figures from + \`query_chinook\`, learn the schema once with \`introspect_schema\` and record it + in your memory, and use \`add_customer\` only when asked to add a genuinely new + customer (a human approves that write).`; + +const INBOX_PROMPT = context` + You are the inbox-manager, the email specialist for the + Chinook Sales Assistant. You own Jane's inbox and are the only agent that + touches it. + + Your tools (MCP, prefixed with the server name "mail"): + - \`mail_list_messages\` — list inbox messages (optionally filtered by a query). + - \`mail_read_message\` — read one message in full by id. + - \`mail_create_draft\` — save a reply to the drafts folder. It NEVER sends. + + When asked to find or read mail, return a tight summary the caller can act on + (sender, subject, and the key content) — not the raw dump. + + When asked to save a draft, just call \`mail_create_draft\` with the given + recipient, subject, and body. Saving a draft pauses automatically for Jane to + approve, edit, or reject — that pause IS the approval, so don't ask for + permission in prose first; make the call. Never invent a send tool; you only + ever create drafts.`; + +const REVIEWER_PROMPT = context` + You are the quote-reviewer. You receive a drafted quote — + line items (description, quantity, unit price, line total), any discount, and + the grand total — and you check it before it goes to the customer. + + Verify: + - The arithmetic: quantity x unit price for each line, and the grand total. + - Internal consistency: any stated discount is actually applied; nothing is + double-counted or missing. + - Plausibility: unit prices look like catalogue prices (tracks are normally + about $0.99); totals aren't off by an order of magnitude. + + Reply concisely: either "Looks correct" with a one-line confirmation, or a + short list of specific corrections. Do not rewrite the customer email — just + review the numbers and terms.`; + +const GENRE_PROMPT = context` + You are a music journalist researching one genre for an + online music distributor's weekly newsletter. + + You will be given a single genre and a private research folder to work in. + + How to work: + 1. Use internet_search to find recent, noteworthy developments in that genre + — new releases, notable artists, trends, or events. Run a few searches. + 2. Save the COMPLETE, verbatim output of ALL your searches to a single file: + write_file("/research//sources.md", ...). Do NOT summarize or trim. + This keeps the bulky material out of the editor's context. + 3. Only then, from what you found, write one tight newsletter segment. + + Return ONLY the finished segment as your reply: + - A markdown section: a "## " heading followed by ~120-180 words. + - Lively but factual; name specific artists and releases. + - Do NOT paste raw search results into your reply — those live in your files.`; export interface BuildSubagentsOptions { enableSearch: boolean; @@ -111,10 +118,10 @@ export function buildSubagents( ): SubAgent[] { const chinookAnalyst: SubAgent = { name: "chinook-analyst", - description: - "Query the Chinook database for catalogue prices, customer records, " + - "purchase history, and territory metrics, and add new customers " + - "(with approval). Delegate all database work here.", + description: context` + Query the Chinook database for catalogue prices, customer records, + purchase history, and territory metrics, and add new customers + (with approval). Delegate all database work here.`, systemPrompt: ANALYST_PROMPT, tools: [queryChinook, introspectSchema, addCustomer], model, @@ -132,19 +139,19 @@ export function buildSubagents( const quoteReviewer: SubAgent = { name: "quote-reviewer", - description: - "Review a drafted quote (line items, discount, total) for correct " + - "arithmetic and sane pricing before it is sent. Send it the numbers.", + description: context` + Review a drafted quote (line items, discount, total) for correct + arithmetic and sane pricing before it is sent. Send it the numbers.`, systemPrompt: REVIEWER_PROMPT, model: strongModel, }; const inboxManager: SubAgent = { name: "inbox-manager", - description: - "Read Jane's inbox and save reply drafts. Delegate any " + - "email work here: finding/reading messages and creating a " + - "draft reply (which pauses for Jane's approval).", + description: context` + Read Jane's inbox and save reply drafts. Delegate any + email work here: finding/reading messages and creating a + draft reply (which pauses for Jane's approval).`, systemPrompt: INBOX_PROMPT, tools: mailTools, model, @@ -161,9 +168,9 @@ export function buildSubagents( const genreResearcher: SubAgent = { name: "genre-researcher", - description: - "Research one music genre and write a short newsletter segment " + - "about what's new in it. Delegate one genre per call.", + description: context` + Research one music genre and write a short newsletter segment + about what's new in it. Delegate one genre per call.`, systemPrompt: GENRE_PROMPT, tools: [internetSearch], model, diff --git a/typescript/m5/sales_assistant/test_diagnostic.ts b/typescript/m5/sales_assistant/test_diagnostic.ts index e677265..6246502 100644 --- a/typescript/m5/sales_assistant/test_diagnostic.ts +++ b/typescript/m5/sales_assistant/test_diagnostic.ts @@ -14,6 +14,7 @@ import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { config } from "dotenv"; +import { context } from "langchain"; import { Client } from "@langchain/langgraph-sdk"; const HERE = dirname(fileURLToPath(import.meta.url)); @@ -139,8 +140,9 @@ async function testAgentsMd(client: Client): Promise { try { const [reply] = await ask( client, - "Repeat the diagnostic token from your operating manual. " + - "It appears as italic text near the top of the file." + context` + Repeat the diagnostic token from your operating manual. + It appears as italic text near the top of the file.` ); const passed = reply.includes("CHINOOK-READY"); console.log("done"); @@ -190,8 +192,9 @@ async function testCodeInterpreter(client: Client): Promise { try { const [reply] = await ask( client, - "Use the code interpreter to calculate: 37 tracks at $0.99 each. " + - "What is the exact total?" + context` + Use the code interpreter to calculate: 37 tracks at $0.99 each. + What is the exact total?` ); const passed = reply.includes("36.63"); console.log("done"); @@ -252,9 +255,9 @@ async function testHitlInterrupt(client: Client): Promise { messages: [ { role: "user", - content: - "Draft a reply to the email from Morgan Vale saying we will get " + - "back to them within 24 hours. Save the draft.", + content: context` + Draft a reply to the email from Morgan Vale saying we will get + back to them within 24 hours. Save the draft.`, }, ], }, @@ -285,8 +288,9 @@ async function testRenderPieChart(client: Client): Promise { const [, messages] = await askInThread( client, thread.thread_id, - "Query the Chinook database for total revenue by genre (top 5 genres). " + - "Call render_pie_chart to save a pie chart as diag_genre_revenue.png." + context` + Query the Chinook database for total revenue by genre (top 5 genres). + Call render_pie_chart to save a pie chart as diag_genre_revenue.png.` ); const toolOuts = toolOutputs(messages, "render_pie_chart"); const exists = existsSync(target); diff --git a/typescript/m5/sales_assistant/tools/chart.ts b/typescript/m5/sales_assistant/tools/chart.ts index 69e488b..7c3ef43 100644 --- a/typescript/m5/sales_assistant/tools/chart.ts +++ b/typescript/m5/sales_assistant/tools/chart.ts @@ -16,8 +16,8 @@ import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { createCanvas } from "canvas"; -import { tool } from "@langchain/core/tools"; import { z } from "zod"; +import { context, tool } from "langchain"; const __dirname = dirname(fileURLToPath(import.meta.url)); const OUTPUTS_DIR = join(__dirname, "..", "outputs"); @@ -112,13 +112,13 @@ export const renderPieChart = tool( }, { name: "render_pie_chart", - description: - "Render a labeled pie chart and save it as a PNG in outputs/. " + - "Args: title (chart title), labels (one per slice, e.g. genre names), " + - "values (one numeric value per label, same length as labels), " + - "filename (e.g. \"territory_chart.png\" — any directory components are " + - "stripped and the extension is forced to .png; the file is always " + - "saved into outputs/).", + description: context` + Render a labeled pie chart and save it as a PNG in outputs/. + Args: title (chart title), labels (one per slice, e.g. genre names), + values (one numeric value per label, same length as labels), + filename (e.g. "territory_chart.png" — any directory components are + stripped and the extension is forced to .png; the file is always + saved into outputs/).`, schema: z.object({ title: z.string(), labels: z.array(z.string()), diff --git a/typescript/m5/sales_assistant/tools/html.ts b/typescript/m5/sales_assistant/tools/html.ts index 483bf36..598ae6f 100644 --- a/typescript/m5/sales_assistant/tools/html.ts +++ b/typescript/m5/sales_assistant/tools/html.ts @@ -10,8 +10,8 @@ */ import { marked } from "marked"; import sanitizeHtml from "sanitize-html"; -import { tool } from "@langchain/core/tools"; import { z } from "zod"; +import { context, tool } from "langchain"; const HTML_TEMPLATE = (title: string, body: string) => ` @@ -44,9 +44,9 @@ export const markdownToHtml = tool( }, { name: "markdown_to_html", - description: - "Convert a Markdown newsletter into a complete, styled HTML page. " + - "Returns the full HTML document as a string.", + description: context` + Convert a Markdown newsletter into a complete, styled HTML page. + Returns the full HTML document as a string.`, schema: z.object({ markdownText: z.string(), title: z.string().default("This Week in Music"), diff --git a/typescript/m5/sales_assistant/tools/search.ts b/typescript/m5/sales_assistant/tools/search.ts index 1749f4d..b5c565e 100644 --- a/typescript/m5/sales_assistant/tools/search.ts +++ b/typescript/m5/sales_assistant/tools/search.ts @@ -7,9 +7,9 @@ * if it's absent the tool is simply not registered (see subagents.ts), so * the rest of the assistant still runs. */ -import { TavilySearchAPIWrapper } from "@langchain/tavily"; -import { tool } from "@langchain/core/tools"; import { z } from "zod"; +import { context, tool } from "langchain"; +import { TavilySearchAPIWrapper } from "@langchain/tavily"; const tavily = new TavilySearchAPIWrapper({ tavilyApiKey: process.env.TAVILY_API_KEY, @@ -21,9 +21,9 @@ export const internetSearch = tool( }, { name: "internet_search", - description: - "Search the web for recent news. Use this to research what's new in a " + - "music genre — new releases, notable artists, trends, and events.", + description: context` + Search the web for recent news. Use this to research what's new in a + music genre — new releases, notable artists, trends, and events.`, schema: z.object({ query: z.string(), maxResults: z.number().default(8), diff --git a/typescript/m5/sales_assistant/tools/sql.ts b/typescript/m5/sales_assistant/tools/sql.ts index 9e41319..df7f230 100644 --- a/typescript/m5/sales_assistant/tools/sql.ts +++ b/typescript/m5/sales_assistant/tools/sql.ts @@ -21,8 +21,8 @@ import { DatabaseSync } from "node:sqlite"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; -import { tool } from "@langchain/core/tools"; import { z } from "zod"; +import { context, tool } from "langchain"; const __dirname = dirname(fileURLToPath(import.meta.url)); // The database ships with the agent under data/. @@ -66,12 +66,12 @@ export const queryChinook = tool( }, { name: "query_chinook", - description: - "Run a read-only SQL SELECT against the Chinook database. Returns a " + - "JSON array of row objects. Only a single SELECT statement is allowed " + - "— any attempt to modify the database is rejected. Use this for all " + - "lookups: catalogue prices, a customer's purchase history, territory " + - "metrics, and so on.", + description: context` + Run a read-only SQL SELECT against the Chinook database. Returns a + JSON array of row objects. Only a single SELECT statement is allowed + — any attempt to modify the database is rejected. Use this for all + lookups: catalogue prices, a customer's purchase history, territory + metrics, and so on.`, schema: z.object({ sql: z.string() }), } ); @@ -96,10 +96,10 @@ export const introspectSchema = tool( }, { name: "introspect_schema", - description: - "Return the full database schema (CREATE statements for every table). " + - "Call this once to learn the schema, then record it in your memory so " + - "you don't have to rediscover it on every task.", + description: context` + Return the full database schema (CREATE statements for every table). + Call this once to learn the schema, then record it in your memory so + you don't have to rediscover it on every task.`, schema: z.object({}), } ); @@ -172,11 +172,11 @@ export const addCustomer = tool( }, { name: "add_customer", - description: - "Add a NEW customer to the database, assigned to the current sales " + - "rep. Use this only after confirming the customer is not already in " + - "the system (search by email or name first). A human approves this " + - "write before it runs. Returns the new CustomerId on success.", + description: context` + Add a NEW customer to the database, assigned to the current sales + rep. Use this only after confirming the customer is not already in + the system (search by email or name first). A human approves this + write before it runs. Returns the new CustomerId on success.`, schema: z.object({ firstName: z.string(), lastName: z.string(),