mirror of
https://github.com/langchain-ai/memory-agent-js.git
synced 2026-07-01 20:04:03 -04:00
Initial commit
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
TAVILY_API_KEY=...
|
||||
|
||||
# To separate your traces from other application
|
||||
LANGCHAIN_PROJECT=data-enrichment
|
||||
# LANGCHAIN_API_KEY=...
|
||||
# LANGCHAIN_TRACING_V2=true
|
||||
|
||||
# The following depend on your selected configuration
|
||||
|
||||
## LLM choice:
|
||||
ANTHROPIC_API_KEY=....
|
||||
FIREWORKS_API_KEY=...
|
||||
OPENAI_API_KEY=...
|
||||
@@ -0,0 +1,62 @@
|
||||
module.exports = {
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"prettier",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 12,
|
||||
parser: "@typescript-eslint/parser",
|
||||
project: "./tsconfig.json",
|
||||
sourceType: "module",
|
||||
},
|
||||
plugins: ["import", "@typescript-eslint", "no-instanceof"],
|
||||
ignorePatterns: [
|
||||
".eslintrc.cjs",
|
||||
"scripts",
|
||||
"node_modules",
|
||||
"dist",
|
||||
"dist-cjs",
|
||||
"*.js",
|
||||
"*.cjs",
|
||||
"*.d.ts",
|
||||
],
|
||||
rules: {
|
||||
"no-process-env": 0,
|
||||
"no-instanceof/no-instanceof": 2,
|
||||
"@typescript-eslint/explicit-module-boundary-types": 0,
|
||||
"@typescript-eslint/no-empty-function": 0,
|
||||
"@typescript-eslint/no-non-null-assertion": 0,
|
||||
"@typescript-eslint/no-shadow": 0,
|
||||
"@typescript-eslint/no-empty-interface": 0,
|
||||
"@typescript-eslint/no-use-before-define": ["error", "nofunc"],
|
||||
"@typescript-eslint/no-unused-vars": ["warn", { args: "none" }],
|
||||
"@typescript-eslint/no-floating-promises": "error",
|
||||
"@typescript-eslint/no-misused-promises": "error",
|
||||
camelcase: 0,
|
||||
"class-methods-use-this": 0,
|
||||
"import/extensions": [2, "ignorePackages"],
|
||||
"import/no-extraneous-dependencies": [
|
||||
"error",
|
||||
{ devDependencies: ["**/*.test.ts"] },
|
||||
],
|
||||
"import/no-unresolved": 0,
|
||||
"import/prefer-default-export": 0,
|
||||
"keyword-spacing": "error",
|
||||
"max-classes-per-file": 0,
|
||||
"max-len": 0,
|
||||
"no-await-in-loop": 0,
|
||||
"no-bitwise": 0,
|
||||
"no-console": 0,
|
||||
"no-restricted-syntax": 0,
|
||||
"no-shadow": 0,
|
||||
"no-continue": 0,
|
||||
"no-underscore-dangle": 0,
|
||||
"no-use-before-define": 0,
|
||||
"no-useless-constructor": 0,
|
||||
"no-return-await": 0,
|
||||
"consistent-return": 0,
|
||||
"no-else-return": 0,
|
||||
"new-cap": ["error", { properties: false, capIsNew: false }],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
# This workflow will run integration tests for the current project once per day
|
||||
|
||||
name: Integration Tests
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "37 14 * * *" # Run at 7:37 AM Pacific Time (14:37 UTC) every day
|
||||
workflow_dispatch: # Allows triggering the workflow manually in GitHub UI
|
||||
|
||||
# If another scheduled run starts while this workflow is still running,
|
||||
# cancel the earlier run in favor of the next run.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
integration-tests:
|
||||
name: Integration Tests
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
node-version: [20.x]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: "yarn"
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
- name: Build project
|
||||
run: yarn build
|
||||
- name: Run integration tests
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }}
|
||||
run: yarn test:int
|
||||
@@ -0,0 +1,56 @@
|
||||
# This workflow will run unit tests for the current project
|
||||
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
workflow_dispatch: # Allows triggering the workflow manually in GitHub UI
|
||||
|
||||
# If another push to the same PR or branch happens while this workflow is still running,
|
||||
# cancel the earlier run in favor of the next run.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
unit-tests:
|
||||
name: Unit Tests
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
node-version: [18.x, 20.x]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: "yarn"
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
- name: Build project
|
||||
run: yarn build
|
||||
|
||||
- name: Lint project
|
||||
run: yarn lint:all
|
||||
|
||||
- name: Check README spelling
|
||||
uses: codespell-project/actions-codespell@v2
|
||||
with:
|
||||
ignore_words_file: .codespellignore
|
||||
path: README.md
|
||||
|
||||
- name: Check code spelling
|
||||
uses: codespell-project/actions-codespell@v2
|
||||
with:
|
||||
ignore_words_file: .codespellignore
|
||||
path: src/
|
||||
|
||||
- name: Run tests
|
||||
env:
|
||||
ANTHROPIC_API_KEY: afakekey
|
||||
TAVILY_API_KEY: anotherfakekey
|
||||
run: yarn test
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
index.cjs
|
||||
index.js
|
||||
index.d.ts
|
||||
node_modules
|
||||
dist
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
.turbo
|
||||
**/.turbo
|
||||
**/.eslintcache
|
||||
|
||||
.env
|
||||
.ipynb_checkpoints
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 LangChain
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,221 @@
|
||||
# LangGraph.js ReAct Memory Agent
|
||||
|
||||
[](https://github.com/langchain-ai/memory-agent-js/actions/workflows/unit-tests.yml)
|
||||
[](https://github.com/langchain-ai/memory-agent-js/actions/workflows/integration-tests.yml)
|
||||
[](https://langgraph-studio.vercel.app/templates/open?githubUrl=https://github.com/langchain-ai/memory-agent-js)
|
||||
|
||||
This repo provides a simple example of a ReAct-style agent with a tool to save memories, implemented in JavaScript. This is a straightforward way to allow an agent to persist important information for later use. In this implementation, we save all memories scoped to a configurable `userId`, enabling the bot to learn and remember a user's preferences across different conversational threads.
|
||||
|
||||

|
||||
|
||||
## Getting Started
|
||||
|
||||
This quickstart will get your memory service deployed on [LangGraph Cloud](https://langchain-ai.github.io/langgraph/cloud/). Once created, you can interact with it from any API.
|
||||
|
||||
Assuming you have already [installed LangGraph Studio](https://github.com/langchain-ai/langgraph-studio?tab=readme-ov-file#download), to set up:
|
||||
|
||||
1. Create a `.env` file.
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
2. Define required API keys in your `.env` file.
|
||||
|
||||
<!--
|
||||
Setup instruction auto-generated by `langgraph template lock`. DO NOT EDIT MANUALLY.
|
||||
-->
|
||||
|
||||
### Setup Model
|
||||
|
||||
The defaults values for `model` are shown below:
|
||||
|
||||
```yaml
|
||||
model: anthropic/claude-3-5-sonnet-20240620
|
||||
```
|
||||
|
||||
Follow the instructions below to get set up, or pick one of the additional options.
|
||||
|
||||
#### Anthropic
|
||||
|
||||
To use Anthropic's chat models:
|
||||
|
||||
1. Sign up for an [Anthropic API key](https://console.anthropic.com/) if you haven't already.
|
||||
2. Once you have your API key, add it to your `.env` file:
|
||||
|
||||
```
|
||||
ANTHROPIC_API_KEY=your-api-key
|
||||
```
|
||||
|
||||
#### OpenAI
|
||||
|
||||
To use OpenAI's chat models:
|
||||
|
||||
1. Sign up for an [OpenAI API key](https://platform.openai.com/signup).
|
||||
2. Once you have your API key, add it to your `.env` file:
|
||||
|
||||
```
|
||||
OPENAI_API_KEY=your-api-key
|
||||
```
|
||||
|
||||
<!--
|
||||
End setup instructions
|
||||
-->
|
||||
|
||||
3. Open in LangGraph studio. Navigate to the `memory_agent` graph and have a conversation with it! Try sending some messages saying your name and other things the bot should remember.
|
||||
|
||||
Assuming the bot saved some memories, create a _new_ thread using the `+` icon. Then chat with the bot again - if you've completed your setup correctly, the bot should now have access to the memories you've saved!
|
||||
|
||||
You can review the saved memories by clicking the "memory" button.
|
||||
|
||||

|
||||
|
||||
## How it works
|
||||
|
||||
This chat bot reads from your memory graph's `Store` to easily list extracted memories. If it calls a tool, LangGraph will route to the `store_memory` node to save the information to the store.
|
||||
|
||||
## How to evaluate
|
||||
|
||||
Memory management can be challenging to get right, especially if you add additional tools for the bot to choose between.
|
||||
To tune the frequency and quality of memories your bot is saving, we recommend starting from an evaluation set, adding to it over time as you find and address common errors in your service.
|
||||
|
||||
We have provided a few example evaluation cases in [the test file here](./tests/agent.int.test.ts). As you can see, the metrics themselves don't have to be terribly complicated, especially not at the outset.
|
||||
|
||||
## How to customize
|
||||
|
||||
1. Customize memory content: we've defined a simple memory structure `content: string, context: string` for each memory, but you could structure them in other ways.
|
||||
2. Provide additional tools: the bot will be more useful if you connect it to other functions.
|
||||
3. Select a different model: We default to anthropic/claude-3-5-sonnet-20240620. You can select a compatible chat model using provider/model-name via configuration. Example: openai/gpt-4.
|
||||
4. Customize the prompts: We provide a default prompt in the [prompts.ts](src/memory_agent/prompts.ts) file. You can easily update this via configuration.
|
||||
|
||||
<!--
|
||||
Configuration auto-generated by `langgraph template lock`. DO NOT EDIT MANUALLY.
|
||||
{
|
||||
"config_schemas": {
|
||||
"agent": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"model": {
|
||||
"type": "string",
|
||||
"default": "anthropic/claude-3-5-sonnet-20240620",
|
||||
"description": "The name of the language model to use for the agent. Should be in the form: provider/model-name.",
|
||||
"environment": [
|
||||
{
|
||||
"value": "anthropic/claude-1.2",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-2.0",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-2.1",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-3-5-sonnet-20240620",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-3-haiku-20240307",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-3-opus-20240229",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-3-sonnet-20240229",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-instant-1.2",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo-0125",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo-0301",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo-0613",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo-1106",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo-16k",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo-16k-0613",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-0125-preview",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-0314",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-0613",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-1106-preview",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-32k",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-32k-0314",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-32k-0613",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-turbo",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-turbo-preview",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-vision-preview",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4o",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4o-mini",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-->
|
||||
+297
@@ -0,0 +1,297 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"id": "09472bfb-2223-4f83-8f98-5a48d29dd89a",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"table_name=<Table.orders: 'orders'> columns=[<Column.id: 'id'>, <Column.expected_delivery_date: 'expected_delivery_date'>, <Column.delivered_at: 'delivered_at'>] conditions=[Condition(column='status', operator=<Operator.eq: '='>, value='fulfilled'), Condition(column='ordered_at', operator=<Operator.ge: '>='>, value='2023-05-01'), Condition(column='ordered_at', operator=<Operator.le: '<='>, value='2023-05-31'), Condition(column='delivered_at', operator=<Operator.gt: '>'>, value=DynamicValue(column_name='expected_delivery_date'))] order_by=<OrderBy.asc: 'asc'>\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"from enum import Enum\n",
|
||||
"from typing import Union\n",
|
||||
"\n",
|
||||
"from langsmith.wrappers import wrap_openai\n",
|
||||
"import openai\n",
|
||||
"from openai import OpenAI\n",
|
||||
"from pydantic import BaseModel\n",
|
||||
"\n",
|
||||
"client = OpenAI()\n",
|
||||
"client = wrap_openai(client)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class Table(str, Enum):\n",
|
||||
" orders = \"orders\"\n",
|
||||
" customers = \"customers\"\n",
|
||||
" products = \"products\"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class Column(str, Enum):\n",
|
||||
" id = \"id\"\n",
|
||||
" status = \"status\"\n",
|
||||
" expected_delivery_date = \"expected_delivery_date\"\n",
|
||||
" delivered_at = \"delivered_at\"\n",
|
||||
" shipped_at = \"shipped_at\"\n",
|
||||
" ordered_at = \"ordered_at\"\n",
|
||||
" canceled_at = \"canceled_at\"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class Operator(str, Enum):\n",
|
||||
" eq = \"=\"\n",
|
||||
" gt = \">\"\n",
|
||||
" lt = \"<\"\n",
|
||||
" le = \"<=\"\n",
|
||||
" ge = \">=\"\n",
|
||||
" ne = \"!=\"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class OrderBy(str, Enum):\n",
|
||||
" asc = \"asc\"\n",
|
||||
" desc = \"desc\"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class DynamicValue(BaseModel):\n",
|
||||
" column_name: str\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class Condition(BaseModel):\n",
|
||||
" column: str\n",
|
||||
" operator: Operator\n",
|
||||
" value: Union[str, int, DynamicValue]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class Query(BaseModel):\n",
|
||||
" table_name: Table\n",
|
||||
" columns: list[Column]\n",
|
||||
" conditions: list[Condition]\n",
|
||||
" order_by: OrderBy\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"completion = client.beta.chat.completions.parse(\n",
|
||||
" model=\"gpt-4o-2024-08-06\",\n",
|
||||
" messages=[\n",
|
||||
" {\n",
|
||||
" \"role\": \"system\",\n",
|
||||
" \"content\": \"You are a helpful assistant. The current date is August 6, 2024. You help users query for the data they are looking for by calling the query function.\",\n",
|
||||
" },\n",
|
||||
" {\n",
|
||||
" \"role\": \"user\",\n",
|
||||
" \"content\": \"look up all my orders in may of last year that were fulfilled but not delivered on time\",\n",
|
||||
" },\n",
|
||||
" ],\n",
|
||||
" tools=[\n",
|
||||
" openai.pydantic_function_tool(Query),\n",
|
||||
" ],\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"print(completion.choices[0].message.tool_calls[0].function.parsed_arguments)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "0d6903df-d580-4988-a412-29acbe5a997d",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing import List\n",
|
||||
"\n",
|
||||
"import ell\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@ell.simple(model=\"gpt-4o-mini\", temperature=1.0, n=10)\n",
|
||||
"def write_ten_drafts(idea: str):\n",
|
||||
" \"\"\"You are an adept story writer. The story should only be 3 paragraphs\"\"\"\n",
|
||||
" return f\"Write a story about {idea}.\"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@ell.simple(model=\"gpt-4o\", temperature=0.1)\n",
|
||||
"def choose_the_best_draft(drafts: List[str]):\n",
|
||||
" \"\"\"You are an expert fiction editor.\"\"\"\n",
|
||||
" return f\"Choose the best draft from the following list: {'\\n'.join(drafts)}.\"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"drafts = write_ten_drafts(idea)\n",
|
||||
"\n",
|
||||
"best_draft = choose_the_best_draft(drafts) # Best of 10 sampling."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"id": "edc5e407-5498-417d-8643-b049eef0af94",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# %pip install -U langchain langchain-openai"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 34,
|
||||
"id": "c6791557-dfdd-489c-a8c7-7f1884cd9d2e",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def simple(**dec_kwargs):\n",
|
||||
" def decorator(fn):\n",
|
||||
" model, msgs = (\n",
|
||||
" init_chat_model(**dec_kwargs),\n",
|
||||
" [(\"system\", getattr(fn, \"__doc__\"))] if getattr(fn, \"__doc__\") else [],\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" def call(*args, **kwargs):\n",
|
||||
" return model.invoke([*msgs, (\"user\", str(fn(*args, **kwargs)))]).content\n",
|
||||
"\n",
|
||||
" return call\n",
|
||||
"\n",
|
||||
" return decorator"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "eb20961e-1715-4d80-a231-abcd61dccece",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"@simple(model=\"gpt-4o-mini\", temperature=1.0, n=10)\n",
|
||||
"def write_ten_drafts(idea: str):\n",
|
||||
" \"\"\"You are an adept story writer. The story should only be 3 paragraphs\"\"\"\n",
|
||||
" return f\"Write a story about {idea}.\"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@simple(model=\"gpt-4o-mini\")\n",
|
||||
"def choose_the_best_draft(drafts: list[str]):\n",
|
||||
" \"\"\"You are an expert fiction editor.\"\"\"\n",
|
||||
" return f\"Choose the best draft from the following list: {drafts}.\""
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 30,
|
||||
"id": "3cb3f496-4054-4081-b7ca-039f80b34557",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import inspect\n",
|
||||
"\n",
|
||||
"from langchain.chat_models import init_chat_model\n",
|
||||
"from langgraph.prebuilt import create_react_agent\n",
|
||||
"\n",
|
||||
"# def _get_inputs(signature, *args, **kwargs):\n",
|
||||
"# \"\"\"Return a dictionary of inputs from the function signature.\"\"\"\n",
|
||||
"# bound = signature.bind_partial(*args, **kwargs)\n",
|
||||
"# bound.apply_defaults()\n",
|
||||
"# arguments = dict(bound.arguments)\n",
|
||||
"# arguments.pop(\"self\", None)\n",
|
||||
"# arguments.pop(\"cls\", None)\n",
|
||||
"# for param_name, param in signature.parameters.items():\n",
|
||||
"# if param.kind == inspect.Parameter.VAR_KEYWORD:\n",
|
||||
"# # Update with the **kwargs, and remove the original entry\n",
|
||||
"# # This is to help flatten out keyword arguments\n",
|
||||
"# if param_name in arguments:\n",
|
||||
"# arguments.update(arguments[param_name])\n",
|
||||
"# arguments.pop(param_name)\n",
|
||||
"\n",
|
||||
"# return arguments\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# def _get_inputs_safe(signature, *args, **kwargs):\n",
|
||||
"# try:\n",
|
||||
"# return _get_inputs(signature, *args, **kwargs)\n",
|
||||
"# except BaseException as e:\n",
|
||||
"# print(e)\n",
|
||||
"# return {\"args\": args, \"kwargs\": kwargs}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def simple(**dec_kwargs):\n",
|
||||
" def decorator(fn):\n",
|
||||
" sysprompt = getattr(fn, \"__doc__\", \"\")\n",
|
||||
" # sig = inspect.signature(fn)\n",
|
||||
" model = init_chat_model(**dec_kwargs)\n",
|
||||
" # agent = create_react_agent(model, [fn])\n",
|
||||
"\n",
|
||||
" def call(*args, **kwargs):\n",
|
||||
" # agent_args = _get_inputs_safe(sig, *args, **kwargs)\n",
|
||||
" resp = fn(*args, **kwargs)\n",
|
||||
" return model.invoke([(\"system\", sysprompt), (\"user\", str(resp))]).content\n",
|
||||
"\n",
|
||||
" return call\n",
|
||||
"\n",
|
||||
" return decorator"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 31,
|
||||
"id": "24ef9043-ff1a-4454-9e6d-c86a2655e2be",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 32,
|
||||
"id": "32a36963-4cd0-476f-903a-064cea9330eb",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 33,
|
||||
"id": "bed51da2-a920-4971-bbae-d84ce99cf884",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"'The phrase \"once upon a time\" is the better draft. It evokes a sense of storytelling and invites the reader into a narrative, while \"I like pie\" is more of a simple statement without much context or depth. \"Once upon a time\" has the potential to lead into a rich and engaging story.'"
|
||||
]
|
||||
},
|
||||
"execution_count": 33,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "56237a71-3446-43d5-bb2e-6cbc0ddf7364",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.2"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
export default {
|
||||
preset: "ts-jest/presets/default-esm",
|
||||
moduleNameMapper: {
|
||||
"^(\\.{1,2}/.*)\\.js$": "$1",
|
||||
},
|
||||
transform: {
|
||||
"^.+\\.tsx?$": [
|
||||
"ts-jest",
|
||||
{
|
||||
useESM: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
extensionsToTreatAsEsm: [".ts"],
|
||||
setupFiles: ["dotenv/config"],
|
||||
passWithNoTests: true,
|
||||
testTimeout: 20_000,
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"node_version": "20",
|
||||
"dockerfile_lines": [],
|
||||
"dependencies": ["."],
|
||||
"graphs": {
|
||||
"agent": "./src/memory_agent/graph.ts:graph"
|
||||
},
|
||||
"env": ".env"
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"name": "memory-agent",
|
||||
"version": "0.0.1",
|
||||
"description": "A ReAct-style agent with a tool to store memories.",
|
||||
"main": "src/chatbot/index.ts",
|
||||
"author": "Brace Sproul",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "yarn@1.22.22",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"clean": "rm -rf dist",
|
||||
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --testPathPattern=\\.test\\.ts$ --testPathIgnorePatterns=\\.int\\.test\\.ts$",
|
||||
"test:int": "node --experimental-vm-modules node_modules/jest/bin/jest.js --testPathPattern=\\.int\\.test\\.ts$",
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint src",
|
||||
"format:check": "prettier --check .",
|
||||
"lint:langgraph-json": "node scripts/checkLanggraphPaths.js",
|
||||
"lint:all": "yarn lint & yarn lint:langgraph-json & yarn format:check",
|
||||
"test:all": "yarn test && yarn test:int && yarn lint:langgraph"
|
||||
},
|
||||
"dependencies": {
|
||||
"@langchain/anthropic": "^0.3.3",
|
||||
"@langchain/aws": "^0.1.0",
|
||||
"@langchain/cohere": "^0.3.0",
|
||||
"@langchain/community": "^0.3.4",
|
||||
"@langchain/core": "^0.3.7",
|
||||
"@langchain/google-genai": "^0.1.0",
|
||||
"@langchain/google-vertexai": "^0.1.0",
|
||||
"@langchain/groq": "^0.1.2",
|
||||
"@langchain/langgraph": "^0.2.11",
|
||||
"@langchain/langgraph-sdk": "^0.0.14",
|
||||
"@langchain/mistralai": "^0.1.1",
|
||||
"@langchain/ollama": "^0.1.0",
|
||||
"@langchain/openai": "^0.3.5",
|
||||
"langchain": "^0.3.2",
|
||||
"langsmith": "^0.1.61",
|
||||
"ts-node": "^10.9.2",
|
||||
"uuid": "^10.0.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.1.0",
|
||||
"@eslint/js": "^9.9.1",
|
||||
"@jest/globals": "^29.7.0",
|
||||
"@tsconfig/recommended": "^1.0.7",
|
||||
"@types/jest": "^29.5.0",
|
||||
"@types/node": "^20.14.8",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.8",
|
||||
"@typescript-eslint/parser": "^5.59.8",
|
||||
"dotenv": "^16.4.5",
|
||||
"eslint": "^8.41.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-no-instanceof": "^1.0.1",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.3.3",
|
||||
"ts-jest": "^29.1.0",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
// Function to check if a file exists
|
||||
function fileExists(filePath) {
|
||||
return fs.existsSync(filePath);
|
||||
}
|
||||
|
||||
// Function to check if an object is exported from a file
|
||||
function isObjectExported(filePath, objectName) {
|
||||
try {
|
||||
const fileContent = fs.readFileSync(filePath, "utf8");
|
||||
const exportRegex = new RegExp(
|
||||
`export\\s+(?:const|let|var)\\s+${objectName}\\s*=|export\\s+\\{[^}]*\\b${objectName}\\b[^}]*\\}`,
|
||||
);
|
||||
return exportRegex.test(fileContent);
|
||||
} catch (error) {
|
||||
console.error(`Error reading file ${filePath}: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Main function to check langgraph.json
|
||||
function checkLanggraphPaths() {
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const langgraphPath = path.join(__dirname, "..", "langgraph.json");
|
||||
|
||||
if (!fileExists(langgraphPath)) {
|
||||
console.error("langgraph.json not found in the root directory");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
const langgraphContent = JSON.parse(fs.readFileSync(langgraphPath, "utf8"));
|
||||
const graphs = langgraphContent.graphs;
|
||||
|
||||
if (!graphs || typeof graphs !== "object") {
|
||||
console.error('Invalid or missing "graphs" object in langgraph.json');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let hasError = false;
|
||||
|
||||
for (const [key, value] of Object.entries(graphs)) {
|
||||
const [filePath, objectName] = value.split(":");
|
||||
const fullPath = path.join(__dirname, "..", filePath);
|
||||
|
||||
if (!fileExists(fullPath)) {
|
||||
console.error(`File not found: ${fullPath}`);
|
||||
hasError = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isObjectExported(fullPath, objectName)) {
|
||||
console.error(
|
||||
`Object "${objectName}" is not exported from ${fullPath}`,
|
||||
);
|
||||
hasError = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log(
|
||||
"All paths in langgraph.json are valid and objects are exported correctly.",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error parsing langgraph.json: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
checkLanggraphPaths();
|
||||
@@ -0,0 +1,21 @@
|
||||
// Define the configurable parameters for the agent
|
||||
|
||||
import { Annotation, LangGraphRunnableConfig } from "@langchain/langgraph";
|
||||
import { SYSTEM_PROMPT } from "./prompts.js";
|
||||
|
||||
export const ConfigurationAnnotation = Annotation.Root({
|
||||
userId: Annotation<string>(),
|
||||
model: Annotation<string>(),
|
||||
systemPrompt: Annotation<string>(),
|
||||
});
|
||||
|
||||
export type Configuration = typeof ConfigurationAnnotation.State;
|
||||
|
||||
export function ensureConfiguration(config?: LangGraphRunnableConfig) {
|
||||
const configurable = config?.configurable || {};
|
||||
return {
|
||||
userId: configurable?.userId || "default",
|
||||
model: configurable?.model || "anthropic/claude-3-5-sonnet-20240620",
|
||||
systemPrompt: configurable?.systemPrompt || SYSTEM_PROMPT,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
// Main graph
|
||||
import {
|
||||
LangGraphRunnableConfig,
|
||||
START,
|
||||
StateGraph,
|
||||
END,
|
||||
} from "@langchain/langgraph";
|
||||
import { BaseMessage, AIMessage } from "@langchain/core/messages";
|
||||
import { initChatModel } from "langchain/chat_models/universal";
|
||||
import { initializeTools } from "./tools.js";
|
||||
import {
|
||||
ConfigurationAnnotation,
|
||||
ensureConfiguration,
|
||||
} from "./configuration.js";
|
||||
import { GraphAnnotation } from "./state.js";
|
||||
import { getStoreFromConfigOrThrow, splitModelAndProvider } from "./utils.js";
|
||||
|
||||
const llm = await initChatModel();
|
||||
|
||||
async function callModel(
|
||||
state: typeof GraphAnnotation.State,
|
||||
config: LangGraphRunnableConfig,
|
||||
): Promise<{ messages: BaseMessage[] }> {
|
||||
const store = getStoreFromConfigOrThrow(config);
|
||||
const configurable = ensureConfiguration(config);
|
||||
const memories = await store.search(["memories", configurable.userId], {
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
let formatted =
|
||||
memories
|
||||
?.map((mem) => `[${mem.key}]: ${JSON.stringify(mem.value)}`)
|
||||
?.join("\n") || "";
|
||||
if (formatted) {
|
||||
formatted = `\n<memories>\n${formatted}\n</memories>`;
|
||||
}
|
||||
|
||||
const sys = configurable.systemPrompt
|
||||
.replace("{user_info}", formatted)
|
||||
.replace("{time}", new Date().toISOString());
|
||||
|
||||
const tools = initializeTools(config);
|
||||
const boundLLM = llm.bind({
|
||||
tools: tools,
|
||||
tool_choice: "auto",
|
||||
});
|
||||
|
||||
const result = await boundLLM.invoke(
|
||||
[{ role: "system", content: sys }, ...state.messages],
|
||||
{
|
||||
configurable: splitModelAndProvider(configurable.model),
|
||||
},
|
||||
);
|
||||
|
||||
return { messages: [result] };
|
||||
}
|
||||
|
||||
async function storeMemory(
|
||||
state: typeof GraphAnnotation.State,
|
||||
config: LangGraphRunnableConfig,
|
||||
): Promise<{ messages: BaseMessage[] }> {
|
||||
const lastMessage = state.messages[state.messages.length - 1] as AIMessage;
|
||||
const toolCalls = lastMessage.tool_calls || [];
|
||||
|
||||
const tools = initializeTools(config);
|
||||
const upsertMemoryTool = tools[0];
|
||||
|
||||
const savedMemories = await Promise.all(
|
||||
toolCalls.map(async (tc) => {
|
||||
return await upsertMemoryTool.invoke(tc);
|
||||
}),
|
||||
);
|
||||
|
||||
return { messages: savedMemories };
|
||||
}
|
||||
|
||||
function routeMessage(
|
||||
state: typeof GraphAnnotation.State,
|
||||
): "store_memory" | typeof END {
|
||||
const lastMessage = state.messages[state.messages.length - 1] as AIMessage;
|
||||
if (lastMessage.tool_calls?.length) {
|
||||
return "store_memory";
|
||||
}
|
||||
return END;
|
||||
}
|
||||
|
||||
// Create the graph + all nodes
|
||||
export const builder = new StateGraph(
|
||||
{
|
||||
stateSchema: GraphAnnotation,
|
||||
},
|
||||
ConfigurationAnnotation,
|
||||
)
|
||||
.addNode("call_model", callModel)
|
||||
.addNode("store_memory", storeMemory)
|
||||
.addEdge(START, "call_model")
|
||||
.addConditionalEdges("call_model", routeMessage, {
|
||||
store_memory: "store_memory",
|
||||
[END]: END,
|
||||
})
|
||||
.addEdge("store_memory", "call_model");
|
||||
|
||||
export const graph = builder.compile();
|
||||
graph.name = "MemoryAgent";
|
||||
@@ -0,0 +1,7 @@
|
||||
// Define default prompts
|
||||
|
||||
export const SYSTEM_PROMPT = `You are a helpful and friendly chatbot. Get to know the user! \
|
||||
Ask questions! Be spontaneous!
|
||||
{user_info}
|
||||
|
||||
System Time: {time}`;
|
||||
@@ -0,0 +1,15 @@
|
||||
import { BaseMessage } from "@langchain/core/messages";
|
||||
import { Annotation, messagesStateReducer } from "@langchain/langgraph";
|
||||
|
||||
/**
|
||||
* Main graph state.
|
||||
*/
|
||||
export const GraphAnnotation = Annotation.Root({
|
||||
/**
|
||||
* The messages in the conversation.
|
||||
*/
|
||||
messages: Annotation<BaseMessage[]>({
|
||||
reducer: messagesStateReducer,
|
||||
default: () => [],
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import { LangGraphRunnableConfig } from "@langchain/langgraph";
|
||||
import { ensureConfiguration } from "./configuration.js";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { tool } from "@langchain/core/tools";
|
||||
import { z } from "zod";
|
||||
import { getStoreFromConfigOrThrow } from "./utils.js";
|
||||
|
||||
/**
|
||||
* Initialize tools within a function so that they have access to the current
|
||||
* state and config at runtime.
|
||||
*/
|
||||
export function initializeTools(config?: LangGraphRunnableConfig) {
|
||||
/**
|
||||
* Upsert a memory in the database.
|
||||
* @param content The main content of the memory.
|
||||
* @param context Additional context for the memory.
|
||||
* @param memoryId Optional ID to overwrite an existing memory.
|
||||
* @returns A string confirming the memory storage.
|
||||
*/
|
||||
async function upsertMemory(opts: {
|
||||
content: string;
|
||||
context: string;
|
||||
memoryId?: string;
|
||||
}): Promise<string> {
|
||||
const { content, context, memoryId } = opts;
|
||||
if (!config || !config.store) {
|
||||
throw new Error("Config or store not provided");
|
||||
}
|
||||
|
||||
const configurable = ensureConfiguration(config);
|
||||
const memId = memoryId || uuidv4();
|
||||
const store = getStoreFromConfigOrThrow(config);
|
||||
|
||||
await store.put(["memories", configurable.userId], memId, {
|
||||
content,
|
||||
context,
|
||||
});
|
||||
|
||||
return `Stored memory ${memId}`;
|
||||
}
|
||||
|
||||
const upsertMemoryTool = tool(upsertMemory, {
|
||||
name: "upsertMemory",
|
||||
description:
|
||||
"Upsert a memory in the database. If a memory conflicts with an existing one, \
|
||||
update the existing one by passing in the memory_id instead of creating a duplicate. \
|
||||
If the user corrects a memory, update it. Can call multiple times in parallel \
|
||||
if you need to store or update multiple memories.",
|
||||
schema: z.object({
|
||||
content: z.string().describe(
|
||||
"The main content of the memory. For example: \
|
||||
'User expressed interest in learning about French.'",
|
||||
),
|
||||
context: z.string().describe(
|
||||
"Additional context for the memory. For example: \
|
||||
'This was mentioned while discussing career options in Europe.'",
|
||||
),
|
||||
memoryId: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"The memory ID to overwrite. Only provide if updating an existing memory.",
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
return [upsertMemoryTool];
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { BaseStore, LangGraphRunnableConfig } from "@langchain/langgraph";
|
||||
/**
|
||||
* Get the store from the configuration or throw an error.
|
||||
*/
|
||||
export function getStoreFromConfigOrThrow(
|
||||
config: LangGraphRunnableConfig,
|
||||
): BaseStore {
|
||||
if (!config.store) {
|
||||
throw new Error("Store not found in configuration");
|
||||
}
|
||||
|
||||
return config.store;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split the fully specified model name into model and provider.
|
||||
*/
|
||||
export function splitModelAndProvider(fullySpecifiedName: string): {
|
||||
model: string;
|
||||
provider?: string;
|
||||
} {
|
||||
let provider: string | undefined;
|
||||
let model: string;
|
||||
|
||||
if (fullySpecifiedName.includes("/")) {
|
||||
[provider, model] = fullySpecifiedName.split("/", 2);
|
||||
} else {
|
||||
model = fullySpecifiedName;
|
||||
}
|
||||
|
||||
return { model, provider };
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 688 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 596 KiB |
@@ -0,0 +1,59 @@
|
||||
import { describe, it, expect } from "@jest/globals";
|
||||
import { MemorySaver, MemoryStore } from "@langchain/langgraph";
|
||||
import { builder } from "../src/memory_agent/graph.js";
|
||||
|
||||
describe("Memory Graph", () => {
|
||||
const conversations = [
|
||||
["My name is Alice and I love pizza. Remember this."],
|
||||
[
|
||||
"Hi, I'm Bob and I enjoy playing tennis. Remember this.",
|
||||
"Yes, I also have a pet dog named Max.",
|
||||
"Max is a golden retriever and he's 5 years old. Please remember this too.",
|
||||
],
|
||||
[
|
||||
"Hello, I'm Charlie. I work as a software engineer and I'm passionate about AI. Remember this.",
|
||||
"I specialize in machine learning algorithms and I'm currently working on a project involving natural language processing.",
|
||||
"My main goal is to improve sentiment analysis accuracy in multi-lingual texts. It's challenging but exciting.",
|
||||
"We've made some progress using transformer models, but we're still working on handling context and idioms across languages.",
|
||||
"Chinese and English have been the most challenging pair so far due to their vast differences in structure and cultural contexts.",
|
||||
],
|
||||
];
|
||||
|
||||
it.each(
|
||||
conversations.map((conversation, index) => [
|
||||
["short", "medium", "long"][index],
|
||||
conversation,
|
||||
]),
|
||||
)(
|
||||
"should store memories for %s conversation",
|
||||
async (_, conversation) => {
|
||||
const memStore = new MemoryStore();
|
||||
const graph = builder.compile({
|
||||
store: memStore,
|
||||
checkpointer: new MemorySaver(),
|
||||
});
|
||||
const userId = "test-user";
|
||||
for (const content of conversation) {
|
||||
await graph.invoke(
|
||||
{
|
||||
messages: [
|
||||
{ role: "user", content: [{ type: "text", text: content }] },
|
||||
],
|
||||
},
|
||||
{
|
||||
configurable: { userId, thread_id: "thread" },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const namespace = ["memories", userId];
|
||||
const memories = await memStore.search(namespace);
|
||||
expect(memories.length).toBeGreaterThan(0);
|
||||
|
||||
const badNamespace = ["memories", "wrong-user"];
|
||||
const badMemories = await memStore.search(badNamespace);
|
||||
expect(badMemories.length).toBe(0);
|
||||
},
|
||||
30000,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { describe, it, expect } from "@jest/globals";
|
||||
import { graph } from "../src/memory_agent/graph.js";
|
||||
|
||||
describe("Memory Graph", () => {
|
||||
it("should initialize and compile the graph", () => {
|
||||
expect(graph).toBeDefined();
|
||||
expect(graph.name).toBe("MemoryAgent");
|
||||
});
|
||||
|
||||
// TODO: Add more test cases for individual nodes, routing logic, tool integration, and output validation
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { describe, it, expect } from "@jest/globals";
|
||||
import { ensureConfiguration } from "../src/memory_agent/configuration.js";
|
||||
|
||||
describe("Configuration", () => {
|
||||
it("should initialize configuration from an empty object", () => {
|
||||
const emptyConfig = {};
|
||||
const result = ensureConfiguration(emptyConfig);
|
||||
expect(result).toBeDefined();
|
||||
expect(typeof result).toBe("object");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"extends": "@tsconfig/recommended",
|
||||
"compilerOptions": {
|
||||
"target": "ES2021",
|
||||
"lib": ["ES2021", "ES2022.Object", "DOM"],
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "nodenext",
|
||||
"esModuleInterop": true,
|
||||
"declaration": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"useDefineForClassFields": true,
|
||||
"strictPropertyInitialization": false,
|
||||
"allowJs": true,
|
||||
"strict": true,
|
||||
"outDir": "dist",
|
||||
"types": ["jest", "node"],
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.js"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user