mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-05 08:06:19 +00:00
feat(sdk-playground): interactive SDK chat playground (#1437)
## Summary - Add `apps/sdk-playground` — chat UI to test TS/Python SDK integrations - Context panel with document memories, API keys in dashboard, tools reference tab - Python FastAPI server on port 8792; portless entry in `portless.json` Stacked on #1436 ## Test plan - [ ] `cd apps/sdk-playground && bun run check-types` - [ ] `bun run dev` with Supermemory + OpenAI keys in UI - [ ] Switch SDKs and verify chat + context panel Made with [Cursor](https://cursor.com)
This commit is contained in:
parent
7974498062
commit
b01d2b69a3
29 changed files with 5768 additions and 10 deletions
45
.github/workflows/ci.yml
vendored
45
.github/workflows/ci.yml
vendored
|
|
@ -26,7 +26,7 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Detect SDK package changes
|
||||
- name: Detect SDK and playground changes
|
||||
id: sdk-changes
|
||||
run: |
|
||||
if git diff --quiet "${{ github.event.pull_request.base.sha }}" HEAD -- packages/tools; then
|
||||
|
|
@ -41,16 +41,45 @@ jobs:
|
|||
echo "ai_sdk=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
if git diff --quiet "${{ github.event.pull_request.base.sha }}" HEAD -- apps/sdk-playground; then
|
||||
echo "sdk_playground=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "sdk_playground=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Setup Python for SDK Playground
|
||||
if: steps.sdk-changes.outputs.sdk_playground == 'true'
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Setup uv for SDK Playground
|
||||
if: steps.sdk-changes.outputs.sdk_playground == 'true'
|
||||
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
||||
with:
|
||||
version: "0.12.5"
|
||||
enable-cache: true
|
||||
working-directory: apps/sdk-playground/python
|
||||
cache-dependency-glob: uv.lock
|
||||
|
||||
- name: Validate SDK Playground Python server
|
||||
if: steps.sdk-changes.outputs.sdk_playground == 'true'
|
||||
working-directory: apps/sdk-playground/python
|
||||
run: |
|
||||
uv sync --locked --python 3.12
|
||||
.venv/bin/python -m py_compile server.py
|
||||
.venv/bin/python -c "import server"
|
||||
|
||||
- name: Run Tools unit tests
|
||||
if: steps.sdk-changes.outputs.tools == 'true'
|
||||
run: bun run --cwd packages/tools test:unit
|
||||
|
||||
- name: Build Tools package
|
||||
if: steps.sdk-changes.outputs.tools == 'true' || steps.sdk-changes.outputs.ai_sdk == 'true'
|
||||
if: steps.sdk-changes.outputs.tools == 'true' || steps.sdk-changes.outputs.ai_sdk == 'true' || steps.sdk-changes.outputs.sdk_playground == 'true'
|
||||
run: bun run --cwd packages/tools build
|
||||
|
||||
- name: Run AI SDK type checking
|
||||
if: steps.sdk-changes.outputs.tools == 'true' || steps.sdk-changes.outputs.ai_sdk == 'true'
|
||||
if: steps.sdk-changes.outputs.tools == 'true' || steps.sdk-changes.outputs.ai_sdk == 'true' || steps.sdk-changes.outputs.sdk_playground == 'true'
|
||||
run: bun run --cwd packages/ai-sdk check-types
|
||||
|
||||
- name: Run AI SDK unit tests
|
||||
|
|
@ -58,9 +87,17 @@ jobs:
|
|||
run: bun run --cwd packages/ai-sdk test:unit
|
||||
|
||||
- name: Build AI SDK package
|
||||
if: steps.sdk-changes.outputs.ai_sdk == 'true'
|
||||
if: steps.sdk-changes.outputs.ai_sdk == 'true' || steps.sdk-changes.outputs.sdk_playground == 'true'
|
||||
run: bun run --cwd packages/ai-sdk build
|
||||
|
||||
- name: Run SDK Playground type checking
|
||||
if: steps.sdk-changes.outputs.sdk_playground == 'true'
|
||||
run: bun run --cwd apps/sdk-playground check-types:app
|
||||
|
||||
- name: Build SDK Playground
|
||||
if: steps.sdk-changes.outputs.sdk_playground == 'true'
|
||||
run: bun run --cwd apps/sdk-playground build:app
|
||||
|
||||
- name: Run Memory Graph type checking
|
||||
run: bun run --cwd packages/memory-graph check-types
|
||||
|
||||
|
|
|
|||
11
apps/sdk-playground/.env.example
Normal file
11
apps/sdk-playground/.env.example
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Local development only. Use disposable development credentials, not production keys.
|
||||
SUPERMEMORY_API_KEY=
|
||||
OPENAI_API_KEY=
|
||||
SUPERMEMORY_BASE_URL=
|
||||
|
||||
# Optional
|
||||
MODEL_NAME=gpt-4o-mini
|
||||
SDK_PLAYGROUND_PYTHON_URL=http://127.0.0.1:8792
|
||||
SDK_PLAYGROUND_PYTHON_PORT=8792
|
||||
# Leave unset unless a trusted non-local development hostname must use env keys.
|
||||
# SDK_PLAYGROUND_ALLOW_ENV_KEYS=true
|
||||
7
apps/sdk-playground/.gitignore
vendored
Normal file
7
apps/sdk-playground/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
.next
|
||||
.env*
|
||||
!.env.example
|
||||
node_modules
|
||||
python/.venv
|
||||
next-env.d.ts
|
||||
*.tsbuildinfo
|
||||
94
apps/sdk-playground/README.md
Normal file
94
apps/sdk-playground/README.md
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
# SDK Agent Playground
|
||||
|
||||
Chat with a **real agent** and switch which Supermemory SDK integration powers it.
|
||||
|
||||
> [!WARNING]
|
||||
> This is a local, single-user development tool. It makes real API calls, stores
|
||||
> browser-entered keys only in memory unless you opt into tab-scoped
|
||||
> `sessionStorage`, and exposes tools that can permanently delete documents. Use
|
||||
> disposable development credentials and a test container; do not deploy it or
|
||||
> point it at production data.
|
||||
|
||||
## Integrations
|
||||
|
||||
| SDK | Style | What happens |
|
||||
|-----|-------|----------------|
|
||||
| AI SDK + middleware | automatic | `withSupermemory` injects context + saves chat |
|
||||
| OpenAI + middleware | automatic | same, via OpenAI client wrapper |
|
||||
| AI SDK + tools | explicit | model calls 7 memory tools via `generateText` |
|
||||
| OpenAI + tools | explicit | OpenAI function-calling loop |
|
||||
| `@supermemory/ai-sdk` | explicit | re-export of tools/ai-sdk |
|
||||
| Python OpenAI middleware | automatic | `with_supermemory` |
|
||||
| Python OpenAI tools | explicit | `SupermemoryTools` loop |
|
||||
| Python supermemory direct | manual | `profile()` + OpenAI + `add()` |
|
||||
|
||||
## Setup
|
||||
|
||||
Prerequisites: Bun 1.3.6, Python 3.11+, and
|
||||
[`uv`](https://docs.astral.sh/uv/). Portless is required only for the HTTPS
|
||||
development hostname; the direct localhost commands below work without it.
|
||||
|
||||
From the repository root:
|
||||
|
||||
```bash
|
||||
bun install --frozen-lockfile
|
||||
|
||||
cp apps/sdk-playground/.env.example apps/sdk-playground/.env.local
|
||||
# Required:
|
||||
# SUPERMEMORY_API_KEY=...
|
||||
# OPENAI_API_KEY=...
|
||||
```
|
||||
|
||||
The playground scripts build `@supermemory/tools` first and
|
||||
`@supermemory/ai-sdk` second before starting, type-checking, or building the
|
||||
Next.js app. Development mode also watches both workspace packages.
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
bun run --cwd apps/sdk-playground dev
|
||||
```
|
||||
|
||||
Opens:
|
||||
|
||||
- **Chat UI** — https://sdk.dev.supermemory.ai via Portless
|
||||
- **Next.js server** — http://127.0.0.1:3005
|
||||
- **Python server** — http://127.0.0.1:8792
|
||||
|
||||
To run without Portless, use two terminals:
|
||||
|
||||
```bash
|
||||
bun run --cwd apps/sdk-playground dev:next
|
||||
bun run --cwd apps/sdk-playground dev:python
|
||||
```
|
||||
|
||||
For a production-mode local smoke check, build first and then start. `start`
|
||||
runs both the built Next.js app and the Python server, and remains intended for
|
||||
local use only.
|
||||
|
||||
```bash
|
||||
bun run --cwd apps/sdk-playground build
|
||||
bun run --cwd apps/sdk-playground start
|
||||
```
|
||||
|
||||
Try:
|
||||
|
||||
- "Remember that I prefer oat milk in coffee"
|
||||
- "What do you know about my drink preferences?"
|
||||
- "Forget that I like tea" (tools mode)
|
||||
|
||||
## Env
|
||||
|
||||
| Variable | Required |
|
||||
|----------|----------|
|
||||
| `SUPERMEMORY_API_KEY` | yes |
|
||||
| `OPENAI_API_KEY` | yes |
|
||||
| `SUPERMEMORY_BASE_URL` | optional |
|
||||
| `MODEL_NAME` | optional (default `gpt-4o-mini`) |
|
||||
| `SDK_PLAYGROUND_PYTHON_URL` | optional (default `http://127.0.0.1:8792`) |
|
||||
| `SDK_PLAYGROUND_PYTHON_PORT` | optional (default `8792`) |
|
||||
| `SDK_PLAYGROUND_ALLOW_ENV_KEYS` | optional; set `true` only when a trusted non-local hostname must use server env keys |
|
||||
|
||||
Server environment keys are exposed to the playground routes only on loopback
|
||||
hosts and `sdk.dev.supermemory.ai` by default. Browser-provided keys remain
|
||||
request-scoped and are never copied into process-global environment variables.
|
||||
7
apps/sdk-playground/next.config.ts
Normal file
7
apps/sdk-playground/next.config.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import type { NextConfig } from "next"
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
transpilePackages: ["@supermemory/tools", "@supermemory/ai-sdk"],
|
||||
}
|
||||
|
||||
export default nextConfig
|
||||
39
apps/sdk-playground/package.json
Normal file
39
apps/sdk-playground/package.json
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"name": "sdk-playground",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"portless": { "name": "sdk.dev.supermemory", "script": "dev:app" },
|
||||
"scripts": {
|
||||
"dev": "portless",
|
||||
"dev:app": "bun run build:dependencies && concurrently -k -n tools,ai-sdk,next,py -c yellow,magenta,blue,green \"bun run --cwd ../../packages/tools dev\" \"bun run --cwd ../../packages/ai-sdk dev\" \"next dev --hostname 127.0.0.1 --port ${PORT:-3005}\" \"bun run dev:python\"",
|
||||
"dev:next": "bun run build:dependencies && next dev --hostname 127.0.0.1 --port ${PORT:-3005}",
|
||||
"dev:python": "cd python && uv run server.py",
|
||||
"build:dependencies": "bun run --cwd ../../packages/tools build && bun run --cwd ../../packages/ai-sdk build",
|
||||
"build:app": "next build",
|
||||
"build": "bun run build:dependencies && bun run build:app",
|
||||
"start": "concurrently -k -n next,py -c blue,green \"next start --hostname 127.0.0.1 --port ${PORT:-3005}\" \"bun run dev:python\"",
|
||||
"typegen": "next typegen",
|
||||
"check-types:app": "bun run typegen && tsc --noEmit --incremental false",
|
||||
"check-types": "bun run build:dependencies && bun run check-types:app"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai": "^2.0.22",
|
||||
"@supermemory/ai-sdk": "workspace:*",
|
||||
"@supermemory/tools": "workspace:*",
|
||||
"ai": "^5.0.113",
|
||||
"next": "16.0.7",
|
||||
"openai": "^4.104.0",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"supermemory": "^4.25.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"concurrently": "^9.1.2",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
7
apps/sdk-playground/postcss.config.mjs
Normal file
7
apps/sdk-playground/postcss.config.mjs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
16
apps/sdk-playground/python/pyproject.toml
Normal file
16
apps/sdk-playground/python/pyproject.toml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
[project]
|
||||
name = "sdk-playground-python"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"fastapi>=0.115.0",
|
||||
"httpx>=0.28.0",
|
||||
"uvicorn[standard]>=0.32.0",
|
||||
"python-dotenv>=1.0.1",
|
||||
"supermemory>=3.50.0",
|
||||
"openai>=1.102.0",
|
||||
"supermemory-openai-sdk[async]",
|
||||
]
|
||||
|
||||
[tool.uv.sources]
|
||||
supermemory-openai-sdk = { path = "../../../packages/openai-sdk-python", editable = true }
|
||||
841
apps/sdk-playground/python/server.py
Normal file
841
apps/sdk-playground/python/server.py
Normal file
|
|
@ -0,0 +1,841 @@
|
|||
"""HTTP server for Python SDK chat integrations in the playground."""
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Any, Literal, Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from fastapi import FastAPI, Header, Query
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel, Field, SecretStr, model_validator
|
||||
from starlette.middleware.trustedhost import TrustedHostMiddleware
|
||||
|
||||
_root = Path(__file__).resolve().parent
|
||||
load_dotenv(_root / ".env")
|
||||
load_dotenv(_root.parent / ".env.local")
|
||||
load_dotenv(_root.parent / ".env")
|
||||
|
||||
DEFAULT_SUPERMEMORY_BASE_URL = "https://api.supermemory.ai"
|
||||
HTTP_TIMEOUT_SECONDS = 60.0
|
||||
CHAT_TIMEOUT_SECONDS = 115.0
|
||||
CONTEXT_DEBUG_TIMEOUT_SECONDS = 10.0
|
||||
DIRECT_SAVE_TIMEOUT_SECONDS = 10.0
|
||||
MAX_OUTPUT_TOKENS = 2_048
|
||||
MAX_MESSAGE_LENGTH = 20_000
|
||||
MAX_MESSAGES = 64
|
||||
MAX_TOTAL_MESSAGE_LENGTH = 100_000
|
||||
MAX_API_KEY_LENGTH = 1_024
|
||||
MAX_CONTAINER_TAG_LENGTH = 100
|
||||
MAX_CONVERSATION_ID_LENGTH = 242
|
||||
CONTAINER_TAG_PATTERN = r"^[a-zA-Z0-9_:-]+$"
|
||||
|
||||
TOOLS_SYSTEM_PROMPT = """You are a helpful assistant with Supermemory long-term memory.
|
||||
|
||||
You have tools to manage memory. Use them proactively:
|
||||
- search_memories: hybrid recall — search before answering whenever user-specific context could help (do not wait to be asked)
|
||||
- get_profile: broad static/dynamic user context at conversation start or when you need a wide overview
|
||||
- add_memory: store a new generalizable fact
|
||||
- document_list / document_add / document_delete: manage source documents
|
||||
- memory_forget: soft-delete one profile fact (not whole documents)
|
||||
|
||||
Before answering questions about the user, their preferences, or past context, search memories or get profile first. When the user asks you to remember something, use add_memory."""
|
||||
|
||||
app = FastAPI(title="SDK Playground Python Chat")
|
||||
app.add_middleware(
|
||||
TrustedHostMiddleware,
|
||||
allowed_hosts=["127.0.0.1", "localhost"],
|
||||
)
|
||||
|
||||
|
||||
class ChatMessage(BaseModel):
|
||||
role: Literal["user", "assistant", "system"]
|
||||
content: str = Field(max_length=MAX_MESSAGE_LENGTH)
|
||||
|
||||
|
||||
class MiddlewareConfig(BaseModel):
|
||||
addMemory: Literal["always", "never"] = "always"
|
||||
verbose: bool = False
|
||||
|
||||
|
||||
class PlaygroundInputError(ValueError):
|
||||
"""A request value is missing after transport-level validation."""
|
||||
|
||||
|
||||
class SupermemoryApiKeys(BaseModel):
|
||||
supermemoryApiKey: SecretStr = Field(max_length=MAX_API_KEY_LENGTH)
|
||||
|
||||
|
||||
class ApiKeys(SupermemoryApiKeys):
|
||||
openaiApiKey: SecretStr = Field(max_length=MAX_API_KEY_LENGTH)
|
||||
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
sdkId: Literal[
|
||||
"py-openai-middleware",
|
||||
"py-openai-tools",
|
||||
"py-supermemory-direct",
|
||||
]
|
||||
messages: list[ChatMessage] = Field(min_length=1, max_length=MAX_MESSAGES)
|
||||
containerTag: str = Field(
|
||||
default="sdk-playground",
|
||||
min_length=1,
|
||||
max_length=MAX_CONTAINER_TAG_LENGTH,
|
||||
pattern=CONTAINER_TAG_PATTERN,
|
||||
)
|
||||
conversationId: str = Field(
|
||||
min_length=1,
|
||||
max_length=MAX_CONVERSATION_ID_LENGTH,
|
||||
)
|
||||
memoryMode: Optional[Literal["profile", "query", "full"]] = "full"
|
||||
middlewareConfig: Optional[MiddlewareConfig] = None
|
||||
apiKeys: Optional[ApiKeys] = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def require_user_message(self) -> "ChatRequest":
|
||||
if not any(
|
||||
message.role == "user" and message.content.strip()
|
||||
for message in self.messages
|
||||
):
|
||||
raise ValueError("messages must include a non-empty user message")
|
||||
if (
|
||||
sum(len(message.content) for message in self.messages)
|
||||
> MAX_TOTAL_MESSAGE_LENGTH
|
||||
):
|
||||
raise ValueError(
|
||||
f"total message content cannot exceed {MAX_TOTAL_MESSAGE_LENGTH} characters"
|
||||
)
|
||||
return self
|
||||
|
||||
|
||||
class ContextRequest(BaseModel):
|
||||
containerTag: str = Field(
|
||||
default="sdk-playground",
|
||||
min_length=1,
|
||||
max_length=MAX_CONTAINER_TAG_LENGTH,
|
||||
pattern=CONTAINER_TAG_PATTERN,
|
||||
)
|
||||
query: Optional[str] = Field(default=None, max_length=MAX_MESSAGE_LENGTH)
|
||||
apiKeys: Optional[SupermemoryApiKeys] = None
|
||||
|
||||
|
||||
def model_name() -> str:
|
||||
return os.getenv("MODEL_NAME", "gpt-4o-mini")
|
||||
|
||||
|
||||
def supplied_secret(value: Optional[SecretStr], label: str) -> str:
|
||||
secret = value.get_secret_value().strip() if value else ""
|
||||
if not secret:
|
||||
raise PlaygroundInputError(f"{label} must be supplied with the request")
|
||||
return secret
|
||||
|
||||
|
||||
def resolve_supermemory_key(api_keys: Optional[SupermemoryApiKeys]) -> str:
|
||||
return supplied_secret(
|
||||
api_keys.supermemoryApiKey if api_keys else None,
|
||||
"Supermemory API key",
|
||||
)
|
||||
|
||||
|
||||
def resolve_chat_keys(api_keys: Optional[ApiKeys]) -> tuple[str, str]:
|
||||
return (
|
||||
resolve_supermemory_key(api_keys),
|
||||
supplied_secret(api_keys.openaiApiKey if api_keys else None, "OpenAI API key"),
|
||||
)
|
||||
|
||||
|
||||
def supermemory_base_url() -> str:
|
||||
configured = os.getenv("SUPERMEMORY_BASE_URL", "").strip()
|
||||
base_url = (configured or DEFAULT_SUPERMEMORY_BASE_URL).rstrip("/")
|
||||
parsed = urlparse(base_url)
|
||||
if parsed.scheme not in ("http", "https") or not parsed.netloc:
|
||||
raise RuntimeError("SUPERMEMORY_BASE_URL must be an absolute HTTP(S) URL")
|
||||
if parsed.username or parsed.password or parsed.query or parsed.fragment:
|
||||
raise RuntimeError(
|
||||
"SUPERMEMORY_BASE_URL cannot contain credentials, a query, or a fragment"
|
||||
)
|
||||
return base_url
|
||||
|
||||
|
||||
def public_error(error: Exception, *secrets: str) -> str:
|
||||
message = str(error)
|
||||
for secret in secrets:
|
||||
if secret:
|
||||
message = message.replace(secret, "[redacted]")
|
||||
return message[:1_000]
|
||||
|
||||
|
||||
async def chat_openai_middleware(
|
||||
messages: list[ChatMessage],
|
||||
container_tag: str,
|
||||
conversation_id: str,
|
||||
memory_mode: str,
|
||||
middleware_config: MiddlewareConfig,
|
||||
sm_key: str,
|
||||
oai_key: str,
|
||||
) -> str:
|
||||
from openai import AsyncOpenAI
|
||||
from supermemory_openai import OpenAIMiddlewareOptions, with_supermemory
|
||||
|
||||
client = with_supermemory(
|
||||
AsyncOpenAI(
|
||||
api_key=oai_key,
|
||||
timeout=HTTP_TIMEOUT_SECONDS,
|
||||
max_retries=1,
|
||||
),
|
||||
OpenAIMiddlewareOptions(
|
||||
container_tag=container_tag,
|
||||
custom_id=conversation_id,
|
||||
mode=memory_mode,
|
||||
add_memory=middleware_config.addMemory,
|
||||
verbose=middleware_config.verbose,
|
||||
api_key=sm_key,
|
||||
base_url=supermemory_base_url(),
|
||||
),
|
||||
)
|
||||
|
||||
openai_messages = [m.model_dump() for m in messages]
|
||||
if not any(m.role == "system" for m in messages):
|
||||
openai_messages.insert(
|
||||
0,
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
"You are a helpful assistant with long-term memory about the user."
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
response = await client.chat.completions.create(
|
||||
model=model_name(),
|
||||
messages=openai_messages,
|
||||
max_completion_tokens=MAX_OUTPUT_TOKENS,
|
||||
)
|
||||
return response.choices[0].message.content or ""
|
||||
|
||||
|
||||
async def chat_openai_tools(
|
||||
messages: list[ChatMessage],
|
||||
container_tag: str,
|
||||
sm_key: str,
|
||||
oai_key: str,
|
||||
) -> tuple[str, list[dict[str, Any]]]:
|
||||
from openai import AsyncOpenAI
|
||||
from supermemory_openai import SupermemoryTools, execute_memory_tool_calls
|
||||
|
||||
openai_client = AsyncOpenAI(
|
||||
api_key=oai_key,
|
||||
timeout=HTTP_TIMEOUT_SECONDS,
|
||||
max_retries=1,
|
||||
)
|
||||
config: dict[str, Any] = {
|
||||
"base_url": supermemory_base_url(),
|
||||
"container_tags": [container_tag],
|
||||
}
|
||||
|
||||
tools = SupermemoryTools(sm_key, config)
|
||||
tool_defs = tools.get_tool_definitions()
|
||||
trace: list[dict[str, Any]] = []
|
||||
|
||||
convo: list[dict[str, Any]] = [
|
||||
{"role": "system", "content": TOOLS_SYSTEM_PROMPT},
|
||||
*[m.model_dump() for m in messages if m.role != "system"],
|
||||
]
|
||||
|
||||
for step in range(8):
|
||||
response = await openai_client.chat.completions.create(
|
||||
model=model_name(),
|
||||
messages=convo,
|
||||
tools=tool_defs,
|
||||
max_completion_tokens=MAX_OUTPUT_TOKENS,
|
||||
)
|
||||
message = response.choices[0].message
|
||||
convo.append(message.model_dump())
|
||||
|
||||
if message.tool_calls:
|
||||
tool_messages = await execute_memory_tool_calls(
|
||||
sm_key,
|
||||
message.tool_calls,
|
||||
config,
|
||||
)
|
||||
for i, call in enumerate(message.tool_calls):
|
||||
raw = tool_messages[i]["content"]
|
||||
try:
|
||||
parsed = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
parsed = raw
|
||||
trace.append(
|
||||
{
|
||||
"step": step + 1,
|
||||
"toolName": call.function.name,
|
||||
"args": json.loads(call.function.arguments),
|
||||
"result": parsed,
|
||||
}
|
||||
)
|
||||
convo.extend(tool_messages)
|
||||
continue
|
||||
|
||||
return message.content or "", trace
|
||||
|
||||
raise RuntimeError("Tool loop exceeded max steps")
|
||||
|
||||
|
||||
def object_field(value: Any, name: str, default: Any = None) -> Any:
|
||||
if isinstance(value, dict):
|
||||
return value.get(name, default)
|
||||
return getattr(value, name, default)
|
||||
|
||||
|
||||
def list_field(value: Any, name: str) -> list[Any]:
|
||||
result = object_field(value, name, [])
|
||||
return result if isinstance(result, list) else []
|
||||
|
||||
|
||||
def extract_profile_context(profile_response: Any) -> dict[str, list[Any]]:
|
||||
profile = object_field(profile_response, "profile", {}) or {}
|
||||
search_results = object_field(profile_response, "search_results", None)
|
||||
if search_results is None and isinstance(profile_response, dict):
|
||||
search_results = profile_response.get("searchResults")
|
||||
|
||||
if isinstance(search_results, list):
|
||||
search_list = search_results
|
||||
else:
|
||||
search_list = list_field(search_results, "results")
|
||||
|
||||
return {
|
||||
"static": list_field(profile, "static"),
|
||||
"dynamic": list_field(profile, "dynamic"),
|
||||
"searchResults": search_list,
|
||||
}
|
||||
|
||||
|
||||
def display_context_item(item: Any) -> str:
|
||||
if hasattr(item, "model_dump"):
|
||||
return json.dumps(item.model_dump(mode="json"), ensure_ascii=False)
|
||||
if isinstance(item, dict):
|
||||
return json.dumps(item, ensure_ascii=False)
|
||||
return str(item)
|
||||
|
||||
|
||||
def direct_conversation_custom_id(conversation_id: str) -> str:
|
||||
readable = re.sub(r"[^A-Za-z0-9._-]+", "-", conversation_id).strip("-._")
|
||||
readable = readable[:40] or "session"
|
||||
digest = hashlib.sha256(conversation_id.encode("utf-8")).hexdigest()[:12]
|
||||
return f"sdk-playground-direct-{readable}-{digest}"
|
||||
|
||||
|
||||
def conversation_transcript(messages: list[ChatMessage], assistant_text: str) -> str:
|
||||
transcript = [
|
||||
f"{message.role.capitalize()}: {message.content}"
|
||||
for message in messages
|
||||
if message.role != "system"
|
||||
]
|
||||
transcript.append(f"Assistant: {assistant_text or '(empty response)'}")
|
||||
return "\n\n".join(transcript)
|
||||
|
||||
|
||||
async def fetch_profile_context(
|
||||
container_tag: str,
|
||||
sm_key: str,
|
||||
query: Optional[str] = None,
|
||||
) -> dict[str, list[Any]]:
|
||||
from supermemory import AsyncSupermemory
|
||||
|
||||
client = AsyncSupermemory(
|
||||
api_key=sm_key,
|
||||
base_url=supermemory_base_url(),
|
||||
timeout=HTTP_TIMEOUT_SECONDS,
|
||||
)
|
||||
profile_response = await client.profile(
|
||||
container_tag=container_tag,
|
||||
**({"q": query} if query else {}),
|
||||
)
|
||||
return extract_profile_context(profile_response)
|
||||
|
||||
|
||||
async def chat_supermemory_direct(
|
||||
messages: list[ChatMessage],
|
||||
container_tag: str,
|
||||
conversation_id: str,
|
||||
sm_key: str,
|
||||
oai_key: str,
|
||||
) -> tuple[str, str, dict[str, list[Any]]]:
|
||||
"""Manual pattern: profile() for context, then OpenAI, then add() conversation."""
|
||||
from openai import AsyncOpenAI
|
||||
from supermemory import AsyncSupermemory
|
||||
|
||||
sm_client = AsyncSupermemory(
|
||||
api_key=sm_key,
|
||||
base_url=supermemory_base_url(),
|
||||
timeout=HTTP_TIMEOUT_SECONDS,
|
||||
)
|
||||
openai_client = AsyncOpenAI(
|
||||
api_key=oai_key,
|
||||
timeout=HTTP_TIMEOUT_SECONDS,
|
||||
max_retries=1,
|
||||
)
|
||||
|
||||
user_messages = [m for m in messages if m.role == "user"]
|
||||
last_user = user_messages[-1].content if user_messages else ""
|
||||
|
||||
profile_response = await sm_client.profile(
|
||||
container_tag=container_tag,
|
||||
**({"q": last_user} if last_user else {}),
|
||||
)
|
||||
profile_context = extract_profile_context(profile_response)
|
||||
context = "\n".join(
|
||||
(
|
||||
"Profile static: "
|
||||
+ ", ".join(map(display_context_item, profile_context["static"])),
|
||||
"Profile dynamic: "
|
||||
+ ", ".join(map(display_context_item, profile_context["dynamic"])),
|
||||
"Relevant search results: "
|
||||
+ ", ".join(map(display_context_item, profile_context["searchResults"])),
|
||||
)
|
||||
)
|
||||
|
||||
openai_messages: list[dict[str, str]] = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": f"You are a helpful assistant. User context:\n{context}",
|
||||
},
|
||||
*[m.model_dump() for m in messages if m.role != "system"],
|
||||
]
|
||||
|
||||
response = await openai_client.chat.completions.create(
|
||||
model=model_name(),
|
||||
messages=openai_messages,
|
||||
max_completion_tokens=MAX_OUTPUT_TOKENS,
|
||||
)
|
||||
assistant_text = response.choices[0].message.content or ""
|
||||
|
||||
custom_id = direct_conversation_custom_id(conversation_id)
|
||||
return assistant_text, custom_id, profile_context
|
||||
|
||||
|
||||
async def save_direct_conversation(
|
||||
messages: list[ChatMessage],
|
||||
assistant_text: str,
|
||||
container_tag: str,
|
||||
custom_id: str,
|
||||
sm_key: str,
|
||||
) -> dict[str, Any]:
|
||||
from supermemory import AsyncSupermemory
|
||||
|
||||
try:
|
||||
client = AsyncSupermemory(
|
||||
api_key=sm_key,
|
||||
base_url=supermemory_base_url(),
|
||||
timeout=DIRECT_SAVE_TIMEOUT_SECONDS,
|
||||
)
|
||||
async with asyncio.timeout(DIRECT_SAVE_TIMEOUT_SECONDS):
|
||||
response = await client.add(
|
||||
content=conversation_transcript(messages, assistant_text),
|
||||
container_tag=container_tag,
|
||||
custom_id=custom_id,
|
||||
)
|
||||
return {
|
||||
"type": "conversation_save_accepted",
|
||||
"label": "Full conversation accepted for processing",
|
||||
"detail": {
|
||||
"nonFatal": True,
|
||||
"containerTag": container_tag,
|
||||
"customId": custom_id,
|
||||
"documentId": object_field(response, "id"),
|
||||
"status": object_field(response, "status"),
|
||||
},
|
||||
}
|
||||
except Exception as error:
|
||||
return {
|
||||
"type": "conversation_save_failed",
|
||||
"label": "Conversation save unavailable",
|
||||
"detail": {
|
||||
"nonFatal": True,
|
||||
"containerTag": container_tag,
|
||||
"customId": custom_id,
|
||||
"error": public_error(error, sm_key),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
async def fetch_container_context(
|
||||
container_tag: str,
|
||||
sm_key: str,
|
||||
query: Optional[str] = None,
|
||||
) -> dict[str, Any]:
|
||||
if not sm_key:
|
||||
raise RuntimeError("Supermemory API key must be supplied")
|
||||
|
||||
profile_context = await fetch_profile_context(container_tag, sm_key, query)
|
||||
base_url = supermemory_base_url()
|
||||
|
||||
import httpx
|
||||
|
||||
async with httpx.AsyncClient(
|
||||
timeout=HTTP_TIMEOUT_SECONDS,
|
||||
follow_redirects=False,
|
||||
) as http:
|
||||
docs_response = await http.post(
|
||||
f"{base_url}/v3/documents/documents",
|
||||
headers={
|
||||
"Authorization": f"Bearer {sm_key}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
json={
|
||||
"containerTags": [container_tag],
|
||||
"limit": 25,
|
||||
"sort": "createdAt",
|
||||
"order": "desc",
|
||||
},
|
||||
)
|
||||
docs_response.raise_for_status()
|
||||
docs = docs_response.json()
|
||||
|
||||
raw_documents = docs.get("documents", []) if isinstance(docs, dict) else []
|
||||
|
||||
documents = []
|
||||
for doc in raw_documents:
|
||||
record = doc if isinstance(doc, dict) else getattr(doc, "__dict__", {})
|
||||
memory_entries = (
|
||||
record.get("memoryEntries") or record.get("memory_entries") or []
|
||||
)
|
||||
if not memory_entries and isinstance(record.get("memories"), list):
|
||||
nested = record.get("memories") or []
|
||||
if nested and isinstance(nested[0], dict) and nested[0].get("memory"):
|
||||
memory_entries = nested
|
||||
documents.append(
|
||||
{
|
||||
"id": record.get("id"),
|
||||
"title": record.get("title"),
|
||||
"status": record.get("status"),
|
||||
"customId": record.get("customId") or record.get("custom_id"),
|
||||
"createdAt": record.get("createdAt") or record.get("created_at"),
|
||||
"updatedAt": record.get("updatedAt") or record.get("updated_at"),
|
||||
"summary": record.get("summary"),
|
||||
"memoryEntries": memory_entries,
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"containerTag": container_tag,
|
||||
"query": query,
|
||||
"profile": profile_context,
|
||||
"documents": documents,
|
||||
"pagination": docs.get("pagination") if isinstance(docs, dict) else None,
|
||||
}
|
||||
|
||||
|
||||
def build_middleware_memory_debug(
|
||||
container_tag: str,
|
||||
conversation_id: str,
|
||||
memory_mode: str,
|
||||
last_user_message: str,
|
||||
context: Optional[dict[str, Any]],
|
||||
context_error: Optional[str],
|
||||
middleware_config: MiddlewareConfig,
|
||||
) -> list[dict[str, Any]]:
|
||||
debug: list[dict[str, Any]] = []
|
||||
if context is None:
|
||||
debug.append(
|
||||
{
|
||||
"type": "context_debug_unavailable",
|
||||
"label": "Post-response context snapshot unavailable",
|
||||
"detail": {"error": context_error or "Unknown context error"},
|
||||
}
|
||||
)
|
||||
else:
|
||||
profile = context["profile"]
|
||||
preview_lines = [
|
||||
f"[memory mode: {memory_mode}]",
|
||||
"[post-response snapshot; not the exact middleware prompt]",
|
||||
]
|
||||
if context.get("query"):
|
||||
preview_lines.append(f"[query: {context['query']}]")
|
||||
|
||||
selected_sections: list[tuple[str, list[Any]]] = []
|
||||
if memory_mode in ("profile", "full"):
|
||||
selected_sections.extend(
|
||||
(
|
||||
("Static", profile.get("static", [])),
|
||||
("Dynamic", profile.get("dynamic", [])),
|
||||
)
|
||||
)
|
||||
if memory_mode in ("query", "full"):
|
||||
selected_sections.append(
|
||||
("Search results", profile.get("searchResults", []))
|
||||
)
|
||||
|
||||
for label, items in selected_sections:
|
||||
if items:
|
||||
preview_lines.append(f"{label}:")
|
||||
for item in items[:8]:
|
||||
preview_lines.append(f"- {display_context_item(item)}")
|
||||
|
||||
debug.extend(
|
||||
(
|
||||
{
|
||||
"type": "profile_fetch",
|
||||
"label": "Post-response profile snapshot",
|
||||
"detail": {
|
||||
"endpoint": "POST /v4/profile",
|
||||
"containerTag": container_tag,
|
||||
"customId": conversation_id,
|
||||
"memoryMode": memory_mode,
|
||||
"query": context.get("query"),
|
||||
"staticCount": len(profile.get("static", [])),
|
||||
"dynamicCount": len(profile.get("dynamic", [])),
|
||||
"searchResultCount": len(profile.get("searchResults", [])),
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "context_preview",
|
||||
"label": "Post-response context preview",
|
||||
"preview": "\n".join(preview_lines),
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
save_detail = {
|
||||
"containerTag": container_tag,
|
||||
"customId": f"conversation:{conversation_id}",
|
||||
"addMemory": middleware_config.addMemory,
|
||||
"verbose": middleware_config.verbose,
|
||||
}
|
||||
if middleware_config.addMemory == "always" and last_user_message.strip():
|
||||
debug.append(
|
||||
{
|
||||
"type": "conversation_save_queued",
|
||||
"label": "Conversation save queued by middleware",
|
||||
"detail": save_detail,
|
||||
}
|
||||
)
|
||||
else:
|
||||
debug.append(
|
||||
{
|
||||
"type": "conversation_save_skipped",
|
||||
"label": "Conversation save disabled",
|
||||
"detail": save_detail,
|
||||
}
|
||||
)
|
||||
return debug
|
||||
|
||||
|
||||
async def fetch_context_for_debug(
|
||||
container_tag: str,
|
||||
query: Optional[str],
|
||||
sm_key: str,
|
||||
) -> tuple[Optional[dict[str, Any]], Optional[str]]:
|
||||
try:
|
||||
async with asyncio.timeout(CONTEXT_DEBUG_TIMEOUT_SECONDS):
|
||||
profile = await fetch_profile_context(container_tag, sm_key, query)
|
||||
return (
|
||||
{
|
||||
"containerTag": container_tag,
|
||||
"query": query,
|
||||
"profile": profile,
|
||||
},
|
||||
None,
|
||||
)
|
||||
except Exception as error:
|
||||
return None, public_error(error, sm_key)
|
||||
|
||||
|
||||
@app.get("/context")
|
||||
async def context_get(
|
||||
containerTag: Annotated[
|
||||
str,
|
||||
Query(
|
||||
min_length=1,
|
||||
max_length=MAX_CONTAINER_TAG_LENGTH,
|
||||
pattern=CONTAINER_TAG_PATTERN,
|
||||
),
|
||||
] = "sdk-playground",
|
||||
query: Annotated[Optional[str], Query(max_length=MAX_MESSAGE_LENGTH)] = None,
|
||||
x_supermemory_api_key: Annotated[
|
||||
Optional[str],
|
||||
Header(alias="X-Supermemory-API-Key"),
|
||||
] = None,
|
||||
):
|
||||
sm_key = ""
|
||||
try:
|
||||
sm_key = supplied_secret(
|
||||
SecretStr(x_supermemory_api_key) if x_supermemory_api_key else None,
|
||||
"X-Supermemory-API-Key header",
|
||||
)
|
||||
async with asyncio.timeout(HTTP_TIMEOUT_SECONDS):
|
||||
ctx = await fetch_container_context(containerTag, sm_key, query)
|
||||
return {"ok": True, "context": ctx}
|
||||
except Exception as error:
|
||||
return JSONResponse(
|
||||
status_code=(
|
||||
504
|
||||
if isinstance(error, TimeoutError)
|
||||
else 400 if isinstance(error, PlaygroundInputError) else 500
|
||||
),
|
||||
content={"ok": False, "error": public_error(error, sm_key)},
|
||||
)
|
||||
|
||||
|
||||
@app.post("/context")
|
||||
async def context_post(req: ContextRequest):
|
||||
sm_key = ""
|
||||
try:
|
||||
sm_key = resolve_supermemory_key(req.apiKeys)
|
||||
async with asyncio.timeout(HTTP_TIMEOUT_SECONDS):
|
||||
ctx = await fetch_container_context(req.containerTag, sm_key, req.query)
|
||||
return {"ok": True, "context": ctx}
|
||||
except Exception as error:
|
||||
return JSONResponse(
|
||||
status_code=(
|
||||
504
|
||||
if isinstance(error, TimeoutError)
|
||||
else 400 if isinstance(error, PlaygroundInputError) else 500
|
||||
),
|
||||
content={"ok": False, "error": public_error(error, sm_key)},
|
||||
)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {
|
||||
"ok": True,
|
||||
"playground": "sdk-playground",
|
||||
"requiresRequestKeys": True,
|
||||
"model": model_name(),
|
||||
"sdks": [
|
||||
"py-openai-middleware",
|
||||
"py-openai-tools",
|
||||
"py-supermemory-direct",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@app.post("/chat")
|
||||
async def chat(req: ChatRequest):
|
||||
started = time.time()
|
||||
sm_key = ""
|
||||
oai_key = ""
|
||||
try:
|
||||
sm_key, oai_key = resolve_chat_keys(req.apiKeys)
|
||||
tool_trace: list[dict[str, Any]] = []
|
||||
memory_debug: list[dict[str, Any]] = []
|
||||
middleware_debug: Optional[tuple[MiddlewareConfig, str, Optional[str]]] = None
|
||||
direct_debug: Optional[tuple[str, dict[str, list[Any]], str]] = None
|
||||
async with asyncio.timeout(CHAT_TIMEOUT_SECONDS):
|
||||
if req.sdkId == "py-openai-middleware":
|
||||
middleware_config = req.middlewareConfig or MiddlewareConfig()
|
||||
text = await chat_openai_middleware(
|
||||
req.messages,
|
||||
req.containerTag,
|
||||
req.conversationId,
|
||||
req.memoryMode or "full",
|
||||
middleware_config,
|
||||
sm_key,
|
||||
oai_key,
|
||||
)
|
||||
last_user = next(
|
||||
(m.content for m in reversed(req.messages) if m.role == "user"),
|
||||
"",
|
||||
)
|
||||
query = last_user if req.memoryMode != "profile" else None
|
||||
middleware_debug = (middleware_config, last_user, query)
|
||||
elif req.sdkId == "py-openai-tools":
|
||||
text, tool_trace = await chat_openai_tools(
|
||||
req.messages, req.containerTag, sm_key, oai_key
|
||||
)
|
||||
elif req.sdkId == "py-supermemory-direct":
|
||||
text, custom_id, profile_context = await chat_supermemory_direct(
|
||||
req.messages,
|
||||
req.containerTag,
|
||||
req.conversationId,
|
||||
sm_key,
|
||||
oai_key,
|
||||
)
|
||||
last_user = next(
|
||||
(m.content for m in reversed(req.messages) if m.role == "user"),
|
||||
"",
|
||||
)
|
||||
direct_debug = (custom_id, profile_context, last_user)
|
||||
else:
|
||||
raise RuntimeError(f"Unsupported Python SDK: {req.sdkId}")
|
||||
|
||||
if middleware_debug is not None:
|
||||
middleware_config, last_user, query = middleware_debug
|
||||
ctx, context_error = await fetch_context_for_debug(
|
||||
req.containerTag,
|
||||
query,
|
||||
sm_key,
|
||||
)
|
||||
memory_debug = build_middleware_memory_debug(
|
||||
req.containerTag,
|
||||
req.conversationId,
|
||||
req.memoryMode or "full",
|
||||
last_user,
|
||||
ctx,
|
||||
context_error,
|
||||
middleware_config,
|
||||
)
|
||||
elif direct_debug is not None:
|
||||
custom_id, profile_context, last_user = direct_debug
|
||||
save_debug = await save_direct_conversation(
|
||||
req.messages,
|
||||
text,
|
||||
req.containerTag,
|
||||
custom_id,
|
||||
sm_key,
|
||||
)
|
||||
memory_debug = [
|
||||
{
|
||||
"type": "manual_profile",
|
||||
"label": "Profile context used for this response",
|
||||
"detail": {
|
||||
"containerTag": req.containerTag,
|
||||
"query": last_user,
|
||||
"staticCount": len(profile_context["static"]),
|
||||
"dynamicCount": len(profile_context["dynamic"]),
|
||||
"searchResultCount": len(profile_context["searchResults"]),
|
||||
},
|
||||
},
|
||||
save_debug,
|
||||
]
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"sdkId": req.sdkId,
|
||||
"message": {"role": "assistant", "content": text},
|
||||
"toolTrace": tool_trace,
|
||||
"memoryDebug": memory_debug,
|
||||
"durationMs": int((time.time() - started) * 1000),
|
||||
}
|
||||
except TimeoutError:
|
||||
return JSONResponse(
|
||||
status_code=504,
|
||||
content={
|
||||
"ok": False,
|
||||
"sdkId": req.sdkId,
|
||||
"error": f"Python chat timed out after {int(CHAT_TIMEOUT_SECONDS)} seconds",
|
||||
"durationMs": int((time.time() - started) * 1000),
|
||||
},
|
||||
)
|
||||
except Exception as error:
|
||||
return JSONResponse(
|
||||
status_code=400 if isinstance(error, PlaygroundInputError) else 500,
|
||||
content={
|
||||
"ok": False,
|
||||
"sdkId": req.sdkId,
|
||||
"error": public_error(error, sm_key, oai_key),
|
||||
"durationMs": int((time.time() - started) * 1000),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
port = int(os.getenv("SDK_PLAYGROUND_PYTHON_PORT", "8792"))
|
||||
uvicorn.run(app, host="127.0.0.1", port=port, log_level="info")
|
||||
1557
apps/sdk-playground/python/uv.lock
generated
Normal file
1557
apps/sdk-playground/python/uv.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
167
apps/sdk-playground/src/app/api/chat/route.ts
Normal file
167
apps/sdk-playground/src/app/api/chat/route.ts
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
import { NextResponse } from "next/server"
|
||||
import { CHAT_SDK_REGISTRY, PYTHON_SERVER_URL } from "@/lib/sdk-registry"
|
||||
import {
|
||||
resolveApiKeys,
|
||||
resolveOpenAiApiKey,
|
||||
resolveSupermemoryApiKey,
|
||||
} from "@/lib/api-keys"
|
||||
import {
|
||||
PlaygroundChatTimeoutError,
|
||||
runTypeScriptChat,
|
||||
} from "@/lib/chat-handlers"
|
||||
import {
|
||||
PlaygroundRequestError,
|
||||
assertTrustedBrowserRequest,
|
||||
mayUseEnvironmentKeys,
|
||||
parseApiKeys,
|
||||
parseContainerTag,
|
||||
parseConversationId,
|
||||
parseIdentifier,
|
||||
parseMemoryMode,
|
||||
parseMessages,
|
||||
parseMiddlewareConfig,
|
||||
readJsonObject,
|
||||
} from "@/lib/request-validation"
|
||||
|
||||
const PYTHON_HEALTH_TIMEOUT_MS = 2_000
|
||||
// Python reserves 115s for the model/tool path and up to 10s for nonfatal debug.
|
||||
const PYTHON_CHAT_TIMEOUT_MS = 130_000
|
||||
|
||||
export async function GET(request: Request) {
|
||||
let pythonOk = false
|
||||
try {
|
||||
const res = await fetch(`${PYTHON_SERVER_URL}/health`, {
|
||||
cache: "no-store",
|
||||
signal: AbortSignal.timeout(PYTHON_HEALTH_TIMEOUT_MS),
|
||||
})
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
pythonOk = data.playground === "sdk-playground"
|
||||
}
|
||||
} catch {
|
||||
pythonOk = false
|
||||
}
|
||||
|
||||
const allowEnvironment = mayUseEnvironmentKeys(request)
|
||||
|
||||
return NextResponse.json({
|
||||
sdks: CHAT_SDK_REGISTRY,
|
||||
hasSupermemoryKey: Boolean(
|
||||
resolveSupermemoryApiKey(null, { allowEnvironment }),
|
||||
),
|
||||
hasOpenAiKey: Boolean(resolveOpenAiApiKey(null, { allowEnvironment })),
|
||||
pythonUrl: PYTHON_SERVER_URL,
|
||||
model: process.env.MODEL_NAME ?? "gpt-4o-mini",
|
||||
pythonOk,
|
||||
})
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const started = Date.now()
|
||||
try {
|
||||
assertTrustedBrowserRequest(req)
|
||||
const body = await readJsonObject(req)
|
||||
const sdkId = parseIdentifier(body.sdkId, "sdkId")
|
||||
const messages = parseMessages(body.messages)
|
||||
const containerTag = parseContainerTag(body.containerTag)
|
||||
const conversationId = parseConversationId(body.conversationId)
|
||||
const memoryMode = parseMemoryMode(body.memoryMode)
|
||||
const middlewareConfig = parseMiddlewareConfig(body.middlewareConfig)
|
||||
const apiKeys = resolveApiKeys(parseApiKeys(body.apiKeys), {
|
||||
allowEnvironment: mayUseEnvironmentKeys(req),
|
||||
})
|
||||
|
||||
if (!apiKeys) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
ok: false,
|
||||
error:
|
||||
"Supermemory and OpenAI API keys are required — enter them in the dashboard or set env vars.",
|
||||
},
|
||||
{ status: 400 },
|
||||
)
|
||||
}
|
||||
|
||||
const sdk = CHAT_SDK_REGISTRY.find((s) => s.id === sdkId)
|
||||
if (!sdk?.available) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: `SDK not available: ${sdkId}` },
|
||||
{ status: 400 },
|
||||
)
|
||||
}
|
||||
|
||||
if (sdk.language === "python") {
|
||||
const res = await fetch(`${PYTHON_SERVER_URL}/chat`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
sdkId,
|
||||
messages,
|
||||
containerTag,
|
||||
conversationId,
|
||||
memoryMode,
|
||||
middlewareConfig,
|
||||
apiKeys,
|
||||
}),
|
||||
signal: AbortSignal.timeout(PYTHON_CHAT_TIMEOUT_MS),
|
||||
})
|
||||
const data = await res.json()
|
||||
if (!res.ok && !data.error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
ok: false,
|
||||
error: `Python server error (${res.status})`,
|
||||
durationMs: Date.now() - started,
|
||||
},
|
||||
{ status: res.status },
|
||||
)
|
||||
}
|
||||
return NextResponse.json(
|
||||
{
|
||||
...data,
|
||||
durationMs: Date.now() - started,
|
||||
},
|
||||
{ status: res.ok ? 200 : res.status },
|
||||
)
|
||||
}
|
||||
|
||||
const result = await runTypeScriptChat(
|
||||
{
|
||||
sdkId,
|
||||
messages,
|
||||
containerTag,
|
||||
conversationId,
|
||||
memoryMode,
|
||||
middlewareConfig,
|
||||
apiKeys,
|
||||
containerTags: [containerTag],
|
||||
},
|
||||
apiKeys,
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
sdkId,
|
||||
message: { role: "assistant", content: result.text },
|
||||
toolTrace: result.toolTrace,
|
||||
memoryDebug: result.memoryDebug,
|
||||
durationMs: Date.now() - started,
|
||||
})
|
||||
} catch (error) {
|
||||
const status =
|
||||
error instanceof PlaygroundRequestError
|
||||
? error.status
|
||||
: error instanceof PlaygroundChatTimeoutError ||
|
||||
(error instanceof Error && error.name === "TimeoutError")
|
||||
? 504
|
||||
: 500
|
||||
return NextResponse.json(
|
||||
{
|
||||
ok: false,
|
||||
durationMs: Date.now() - started,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
{ status },
|
||||
)
|
||||
}
|
||||
}
|
||||
90
apps/sdk-playground/src/app/api/context/route.ts
Normal file
90
apps/sdk-playground/src/app/api/context/route.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import { NextResponse } from "next/server"
|
||||
import { resolveSupermemoryApiKey } from "@/lib/api-keys"
|
||||
import { fetchContainerContext } from "@/lib/context-api"
|
||||
import {
|
||||
PlaygroundRequestError,
|
||||
assertTrustedBrowserRequest,
|
||||
mayUseEnvironmentKeys,
|
||||
parseApiKeys,
|
||||
parseContainerTag,
|
||||
parseOptionalText,
|
||||
readJsonObject,
|
||||
} from "@/lib/request-validation"
|
||||
|
||||
export async function GET(req: Request) {
|
||||
try {
|
||||
assertTrustedBrowserRequest(req)
|
||||
const { searchParams } = new URL(req.url)
|
||||
const containerTag = parseContainerTag(searchParams.get("containerTag"))
|
||||
const query = parseOptionalText(searchParams.get("query"), "query")
|
||||
const supermemoryApiKey = resolveSupermemoryApiKey(null, {
|
||||
allowEnvironment: mayUseEnvironmentKeys(req),
|
||||
})
|
||||
|
||||
if (!supermemoryApiKey) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
ok: false,
|
||||
error:
|
||||
"Supermemory API key is required — enter it in the dashboard or set a local env var.",
|
||||
},
|
||||
{ status: 400 },
|
||||
)
|
||||
}
|
||||
|
||||
const context = await fetchContainerContext(
|
||||
containerTag,
|
||||
query,
|
||||
supermemoryApiKey,
|
||||
)
|
||||
return NextResponse.json({ ok: true, context })
|
||||
} catch (error) {
|
||||
const status = error instanceof PlaygroundRequestError ? error.status : 500
|
||||
return NextResponse.json(
|
||||
{
|
||||
ok: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
{ status },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
assertTrustedBrowserRequest(req)
|
||||
const body = await readJsonObject(req)
|
||||
const containerTag = parseContainerTag(body.containerTag)
|
||||
const query = parseOptionalText(body.query, "query")
|
||||
const supermemoryApiKey = resolveSupermemoryApiKey(
|
||||
parseApiKeys(body.apiKeys),
|
||||
{ allowEnvironment: mayUseEnvironmentKeys(req) },
|
||||
)
|
||||
|
||||
if (!supermemoryApiKey) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
ok: false,
|
||||
error: "Supermemory API key is required — enter it in the dashboard.",
|
||||
},
|
||||
{ status: 400 },
|
||||
)
|
||||
}
|
||||
|
||||
const context = await fetchContainerContext(
|
||||
containerTag,
|
||||
query,
|
||||
supermemoryApiKey,
|
||||
)
|
||||
return NextResponse.json({ ok: true, context })
|
||||
} catch (error) {
|
||||
const status = error instanceof PlaygroundRequestError ? error.status : 500
|
||||
return NextResponse.json(
|
||||
{
|
||||
ok: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
{ status },
|
||||
)
|
||||
}
|
||||
}
|
||||
18
apps/sdk-playground/src/app/globals.css
Normal file
18
apps/sdk-playground/src/app/globals.css
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family:
|
||||
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
textarea,
|
||||
select,
|
||||
input,
|
||||
button {
|
||||
font: inherit;
|
||||
}
|
||||
19
apps/sdk-playground/src/app/layout.tsx
Normal file
19
apps/sdk-playground/src/app/layout.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import type { Metadata } from "next"
|
||||
import "./globals.css"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Supermemory SDK Playground",
|
||||
description: "Switch and test Supermemory SDKs across languages",
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{ children: React.ReactNode }>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className="min-h-screen bg-zinc-950 text-zinc-100 antialiased">
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
623
apps/sdk-playground/src/app/page.tsx
Normal file
623
apps/sdk-playground/src/app/page.tsx
Normal file
|
|
@ -0,0 +1,623 @@
|
|||
"use client"
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { ApiKeysPanel } from "@/components/ApiKeysPanel"
|
||||
import { ContextPanel } from "@/components/ContextPanel"
|
||||
import { ToolsReferencePanel } from "@/components/ToolsReferencePanel"
|
||||
import type { MemoryDebugEntry } from "@/lib/context-api"
|
||||
import {
|
||||
DEFAULT_MIDDLEWARE_CONFIG,
|
||||
type MiddlewareRuntimeConfig,
|
||||
} from "@/lib/middleware-config"
|
||||
import {
|
||||
CHAT_SDK_REGISTRY,
|
||||
type ChatSdkDefinition,
|
||||
type ToolTraceEntry,
|
||||
} from "@/lib/sdk-registry"
|
||||
|
||||
type UserOrAssistantMessage = {
|
||||
kind: "user" | "assistant"
|
||||
content: string
|
||||
}
|
||||
|
||||
type ToolMessage = {
|
||||
kind: "tool"
|
||||
entry: ToolTraceEntry
|
||||
}
|
||||
|
||||
type DebugMessage = {
|
||||
kind: "debug"
|
||||
entry: MemoryDebugEntry
|
||||
}
|
||||
|
||||
type DisplayMessage = UserOrAssistantMessage | ToolMessage | DebugMessage
|
||||
|
||||
export default function AgentPlaygroundPage() {
|
||||
const [sdks, setSdks] = useState(CHAT_SDK_REGISTRY)
|
||||
const [hasSupermemoryKey, setHasSupermemoryKey] = useState(false)
|
||||
const [hasOpenAiKey, setHasOpenAiKey] = useState(false)
|
||||
const [pythonOk, setPythonOk] = useState(false)
|
||||
const [model, setModel] = useState("gpt-4o-mini")
|
||||
const [pythonUrl, setPythonUrl] = useState("http://127.0.0.1:8792")
|
||||
|
||||
const [supermemoryApiKey, setSupermemoryApiKey] = useState("")
|
||||
const [openaiApiKey, setOpenaiApiKey] = useState("")
|
||||
const apiKeys = useMemo(
|
||||
() => ({ supermemoryApiKey, openaiApiKey }),
|
||||
[supermemoryApiKey, openaiApiKey],
|
||||
)
|
||||
const supermemoryKeyReady =
|
||||
supermemoryApiKey.trim().length > 0 || hasSupermemoryKey
|
||||
const openAiKeyReady = openaiApiKey.trim().length > 0 || hasOpenAiKey
|
||||
const keysReady = supermemoryKeyReady && openAiKeyReady
|
||||
|
||||
const [sdkId, setSdkId] = useState("ts-ai-sdk-middleware")
|
||||
const [containerTag, setContainerTag] = useState("sdk-playground")
|
||||
const [memoryMode, setMemoryMode] = useState<"profile" | "query" | "full">(
|
||||
"full",
|
||||
)
|
||||
const [conversationId, setConversationId] = useState("")
|
||||
const [middlewareConfig, setMiddlewareConfig] =
|
||||
useState<MiddlewareRuntimeConfig>(DEFAULT_MIDDLEWARE_CONFIG)
|
||||
|
||||
const [messages, setMessages] = useState<DisplayMessage[]>([])
|
||||
const [input, setInput] = useState("")
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [contextRefreshKey, setContextRefreshKey] = useState(0)
|
||||
const [leftPanel, setLeftPanel] = useState<"sdks" | "tools">("sdks")
|
||||
|
||||
const lastUserMessage = useMemo(() => {
|
||||
const users = messages.filter(
|
||||
(m): m is UserOrAssistantMessage => m.kind === "user",
|
||||
)
|
||||
return users.at(-1)?.content ?? ""
|
||||
}, [messages])
|
||||
|
||||
const selectedSdk = useMemo(
|
||||
() => sdks.find((s) => s.id === sdkId),
|
||||
[sdks, sdkId],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setConversationId((current) => current || crypto.randomUUID())
|
||||
}, [])
|
||||
|
||||
const refreshMeta = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch("/api/chat")
|
||||
const data = await res.json()
|
||||
if (data.sdks) setSdks(data.sdks)
|
||||
setHasSupermemoryKey(Boolean(data.hasSupermemoryKey))
|
||||
setHasOpenAiKey(Boolean(data.hasOpenAiKey))
|
||||
setPythonOk(Boolean(data.pythonOk))
|
||||
if (data.pythonUrl) setPythonUrl(data.pythonUrl)
|
||||
if (data.model) setModel(data.model)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
refreshMeta()
|
||||
}, [refreshMeta])
|
||||
|
||||
const send = async () => {
|
||||
if (!input.trim() || loading || !selectedSdk?.available || !keysReady)
|
||||
return
|
||||
const activeConversationId = conversationId.trim() || crypto.randomUUID()
|
||||
if (!conversationId.trim()) setConversationId(activeConversationId)
|
||||
|
||||
const userMessage: UserOrAssistantMessage = {
|
||||
kind: "user",
|
||||
content: input.trim(),
|
||||
}
|
||||
const chatHistory = messages
|
||||
.filter(
|
||||
(m): m is UserOrAssistantMessage =>
|
||||
m.kind === "user" || m.kind === "assistant",
|
||||
)
|
||||
.map((m) => ({ role: m.kind, content: m.content }))
|
||||
const nextMessages = [...messages, userMessage]
|
||||
setMessages(nextMessages)
|
||||
setInput("")
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/chat", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
sdkId,
|
||||
messages: [
|
||||
...chatHistory,
|
||||
{ role: "user", content: userMessage.content },
|
||||
],
|
||||
containerTag,
|
||||
conversationId: activeConversationId,
|
||||
memoryMode:
|
||||
selectedSdk.mode === "middleware" ? memoryMode : undefined,
|
||||
middlewareConfig:
|
||||
selectedSdk.mode === "middleware" ? middlewareConfig : undefined,
|
||||
apiKeys,
|
||||
}),
|
||||
})
|
||||
const data = await res.json()
|
||||
if (!data.ok) {
|
||||
throw new Error(data.error ?? "Chat failed")
|
||||
}
|
||||
const content = data.message?.content ?? ""
|
||||
const toolTrace = (data.toolTrace ?? []) as ToolTraceEntry[]
|
||||
const memoryDebug = (data.memoryDebug ?? []) as MemoryDebugEntry[]
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
...memoryDebug.map((entry) => ({ kind: "debug" as const, entry })),
|
||||
...toolTrace.map((entry) => ({ kind: "tool" as const, entry })),
|
||||
{ kind: "assistant", content },
|
||||
])
|
||||
setContextRefreshKey((k) => k + 1)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : String(err))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const tsSdks = sdks.filter((s) => s.language === "typescript")
|
||||
const pySdks = sdks.filter((s) => s.language === "python")
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex h-screen max-w-[1400px] flex-col p-4 md:p-5">
|
||||
<header className="mb-4 shrink-0 space-y-3 border-b border-zinc-800 pb-4">
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold tracking-tight">
|
||||
Supermemory Agent Playground
|
||||
</h1>
|
||||
<p className="text-sm text-zinc-400">
|
||||
Talk to a real agent. Switch the underlying SDK integration in the
|
||||
sidebar — middleware auto-injects memory; tools let the model call
|
||||
memory operations explicitly.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 text-xs">
|
||||
<Status ok={supermemoryKeyReady} label="Supermemory key" />
|
||||
<Status ok={openAiKeyReady} label="OpenAI key" />
|
||||
<Status
|
||||
ok={pythonOk}
|
||||
label={`Python ${pythonUrl.replace("http://", "")}`}
|
||||
/>
|
||||
<span className="rounded-full border border-zinc-700 px-3 py-1 text-zinc-400">
|
||||
model: {model}
|
||||
</span>
|
||||
</div>
|
||||
<ApiKeysPanel
|
||||
supermemoryApiKey={supermemoryApiKey}
|
||||
openaiApiKey={openaiApiKey}
|
||||
hasSupermemoryEnvKey={hasSupermemoryKey}
|
||||
hasOpenAiEnvKey={hasOpenAiKey}
|
||||
onSupermemoryChange={setSupermemoryApiKey}
|
||||
onOpenAiChange={setOpenaiApiKey}
|
||||
/>
|
||||
</header>
|
||||
|
||||
<div className="flex min-h-0 flex-1 gap-3 lg:gap-4">
|
||||
<aside className="hidden w-60 shrink-0 min-h-0 lg:flex lg:flex-col">
|
||||
<div className="mb-2 flex gap-1 shrink-0">
|
||||
<SidebarTab
|
||||
active={leftPanel === "sdks"}
|
||||
onClick={() => setLeftPanel("sdks")}
|
||||
>
|
||||
SDKs
|
||||
</SidebarTab>
|
||||
<SidebarTab
|
||||
active={leftPanel === "tools"}
|
||||
onClick={() => setLeftPanel("tools")}
|
||||
>
|
||||
Tools
|
||||
</SidebarTab>
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto">
|
||||
{leftPanel === "sdks" ? (
|
||||
<div className="space-y-4">
|
||||
<Section title="TypeScript">
|
||||
<SdkButtons
|
||||
sdks={tsSdks}
|
||||
selected={sdkId}
|
||||
onSelect={setSdkId}
|
||||
/>
|
||||
</Section>
|
||||
<Section title="Python">
|
||||
<SdkButtons
|
||||
sdks={pySdks}
|
||||
selected={sdkId}
|
||||
onSelect={setSdkId}
|
||||
/>
|
||||
</Section>
|
||||
</div>
|
||||
) : (
|
||||
<ToolsReferencePanel />
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div className="flex min-h-0 flex-1 flex-col gap-3">
|
||||
<div className="flex gap-1 shrink-0 lg:hidden">
|
||||
<SidebarTab
|
||||
active={leftPanel === "sdks"}
|
||||
onClick={() => setLeftPanel("sdks")}
|
||||
>
|
||||
Chat
|
||||
</SidebarTab>
|
||||
<SidebarTab
|
||||
active={leftPanel === "tools"}
|
||||
onClick={() => setLeftPanel("tools")}
|
||||
>
|
||||
Tool reference
|
||||
</SidebarTab>
|
||||
</div>
|
||||
|
||||
{leftPanel === "tools" && (
|
||||
<div className="max-h-72 shrink-0 overflow-hidden rounded-lg border border-zinc-800 bg-zinc-900/20 p-3 lg:hidden">
|
||||
<ToolsReferencePanel />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{leftPanel === "sdks" && (
|
||||
<div className="flex flex-wrap items-end gap-3 md:hidden">
|
||||
<label className="flex-1 space-y-1">
|
||||
<span className="text-xs text-zinc-500">SDK</span>
|
||||
<select
|
||||
value={sdkId}
|
||||
onChange={(e) => setSdkId(e.target.value)}
|
||||
className="w-full rounded-md border border-zinc-700 bg-zinc-900 px-2 py-2 text-sm"
|
||||
>
|
||||
{sdks.map((s) => (
|
||||
<option key={s.id} value={s.id} disabled={!s.available}>
|
||||
{s.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedSdk && (
|
||||
<div className="rounded-lg border border-zinc-800 bg-zinc-900/40 px-3 py-2 text-sm">
|
||||
<div className="font-medium">{selectedSdk.label}</div>
|
||||
<div className="text-zinc-400">{selectedSdk.description}</div>
|
||||
<div className="mt-1 text-xs text-zinc-500">
|
||||
{selectedSdk.package} ·{" "}
|
||||
<span className="text-zinc-400">
|
||||
{selectedSdk.mode === "middleware"
|
||||
? "automatic memory"
|
||||
: selectedSdk.mode === "tools"
|
||||
? "explicit tools"
|
||||
: "manual profile + save"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<label className="space-y-1">
|
||||
<span className="text-xs text-zinc-500">Container tag</span>
|
||||
<input
|
||||
value={containerTag}
|
||||
onChange={(e) => setContainerTag(e.target.value)}
|
||||
maxLength={100}
|
||||
className="rounded-md border border-zinc-700 bg-zinc-900 px-3 py-1.5 text-sm w-40"
|
||||
/>
|
||||
</label>
|
||||
<label className="space-y-1">
|
||||
<span className="text-xs text-zinc-500">customId (session)</span>
|
||||
<input
|
||||
value={conversationId}
|
||||
onChange={(e) => setConversationId(e.target.value)}
|
||||
maxLength={242}
|
||||
className="rounded-md border border-zinc-700 bg-zinc-900 px-3 py-1.5 text-sm font-mono w-52"
|
||||
/>
|
||||
</label>
|
||||
{selectedSdk?.mode === "middleware" && (
|
||||
<>
|
||||
<label className="space-y-1">
|
||||
<span className="text-xs text-zinc-500">Memory mode</span>
|
||||
<select
|
||||
value={memoryMode}
|
||||
onChange={(e) =>
|
||||
setMemoryMode(
|
||||
e.target.value as "profile" | "query" | "full",
|
||||
)
|
||||
}
|
||||
className="rounded-md border border-zinc-700 bg-zinc-900 px-3 py-1.5 text-sm"
|
||||
>
|
||||
<option value="profile">profile</option>
|
||||
<option value="query">query</option>
|
||||
<option value="full">full</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="space-y-1">
|
||||
<span className="text-xs text-zinc-500">addMemory</span>
|
||||
<select
|
||||
value={middlewareConfig.addMemory}
|
||||
onChange={(e) =>
|
||||
setMiddlewareConfig((prev) => ({
|
||||
...prev,
|
||||
addMemory: e.target.value as "always" | "never",
|
||||
}))
|
||||
}
|
||||
className="rounded-md border border-zinc-700 bg-zinc-900 px-3 py-1.5 text-sm"
|
||||
>
|
||||
<option value="always">always</option>
|
||||
<option value="never">never</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="flex items-end gap-2 pb-1.5 text-xs text-zinc-400">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={middlewareConfig.verbose}
|
||||
onChange={(e) =>
|
||||
setMiddlewareConfig((prev) => ({
|
||||
...prev,
|
||||
verbose: e.target.checked,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
verbose
|
||||
</label>
|
||||
{selectedSdk.id === "ts-ai-sdk-middleware" && (
|
||||
<>
|
||||
<label className="flex items-end gap-2 pb-1.5 text-xs text-zinc-400">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={middlewareConfig.includeToolCalls}
|
||||
onChange={(e) =>
|
||||
setMiddlewareConfig((prev) => ({
|
||||
...prev,
|
||||
includeToolCalls: e.target.checked,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
includeToolCalls
|
||||
</label>
|
||||
<label className="flex items-end gap-2 pb-1.5 text-xs text-zinc-400">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={middlewareConfig.skipMemoryOnError}
|
||||
onChange={(e) =>
|
||||
setMiddlewareConfig((prev) => ({
|
||||
...prev,
|
||||
skipMemoryOnError: e.target.checked,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
skipMemoryOnError
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto rounded-lg border border-zinc-800 bg-zinc-900/30 p-4 space-y-3">
|
||||
{messages.length === 0 && (
|
||||
<p className="text-center text-sm text-zinc-500 py-8">
|
||||
Say hi — try "Remember that I prefer oat milk" or
|
||||
"What do you know about me?"
|
||||
</p>
|
||||
)}
|
||||
{messages.map((m, i) => {
|
||||
if (m.kind === "debug") {
|
||||
return (
|
||||
<div key={i} className="flex justify-start">
|
||||
<div className="max-w-[92%] rounded-lg border border-violet-900/50 bg-violet-950/25 px-3 py-2 text-xs">
|
||||
<div className="font-medium text-violet-300 mb-1">
|
||||
Debug · {m.entry.label}
|
||||
</div>
|
||||
{m.entry.detail && (
|
||||
<pre className="font-mono text-violet-100/70 whitespace-pre-wrap break-all mb-2">
|
||||
{JSON.stringify(m.entry.detail, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
{m.entry.preview && (
|
||||
<pre className="font-mono text-zinc-300 whitespace-pre-wrap break-all border-t border-violet-900/40 pt-2 mt-1">
|
||||
{m.entry.preview}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (m.kind === "tool") {
|
||||
return (
|
||||
<div key={i} className="flex justify-start">
|
||||
<div className="max-w-[90%] rounded-lg border border-amber-900/50 bg-amber-950/30 px-3 py-2 text-xs font-mono text-amber-100/90">
|
||||
<div className="font-sans text-amber-400 font-medium mb-1">
|
||||
Tool · step {m.entry.step} · {m.entry.toolName}
|
||||
</div>
|
||||
<div className="text-zinc-400">args</div>
|
||||
<pre className="whitespace-pre-wrap break-all mb-2">
|
||||
{JSON.stringify(m.entry.args, null, 2)}
|
||||
</pre>
|
||||
{m.entry.result !== undefined && (
|
||||
<>
|
||||
<div className="text-zinc-400">result</div>
|
||||
<pre className="whitespace-pre-wrap break-all">
|
||||
{JSON.stringify(m.entry.result, null, 2)}
|
||||
</pre>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className={
|
||||
m.kind === "user"
|
||||
? "flex justify-end"
|
||||
: "flex justify-start"
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={`max-w-[85%] rounded-2xl px-4 py-2 text-sm leading-relaxed ${
|
||||
m.kind === "user"
|
||||
? "bg-emerald-700 text-white"
|
||||
: "bg-zinc-800 text-zinc-100"
|
||||
}`}
|
||||
>
|
||||
{m.content}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{loading && (
|
||||
<div className="text-sm text-zinc-500 animate-pulse">
|
||||
Thinking…
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-md border border-red-900 bg-red-950/50 px-3 py-2 text-sm text-red-300">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2 shrink-0">
|
||||
<input
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
send()
|
||||
}
|
||||
}}
|
||||
disabled={loading || !selectedSdk?.available || !keysReady}
|
||||
placeholder={
|
||||
keysReady
|
||||
? "Message the agent…"
|
||||
: "Enter API keys above to chat…"
|
||||
}
|
||||
className="flex-1 rounded-lg border border-zinc-700 bg-zinc-900 px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-emerald-600/50 disabled:opacity-50"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={send}
|
||||
disabled={
|
||||
loading ||
|
||||
!input.trim() ||
|
||||
!selectedSdk?.available ||
|
||||
!keysReady
|
||||
}
|
||||
className="rounded-lg bg-emerald-600 px-5 py-3 text-sm font-medium text-white hover:bg-emerald-500 disabled:opacity-40"
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside className="hidden w-80 shrink-0 overflow-hidden rounded-lg border border-zinc-800 bg-zinc-900/20 p-3 xl:flex xl:w-96 xl:flex-col min-h-0">
|
||||
<ContextPanel
|
||||
containerTag={containerTag}
|
||||
lastUserMessage={lastUserMessage}
|
||||
refreshKey={contextRefreshKey}
|
||||
supermemoryApiKey={supermemoryApiKey}
|
||||
supermemoryKeyReady={supermemoryKeyReady}
|
||||
/>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Status({ ok, label }: { ok: boolean; label: string }) {
|
||||
return (
|
||||
<span
|
||||
className={`rounded-full border px-3 py-1 ${
|
||||
ok
|
||||
? "border-emerald-800 text-emerald-400"
|
||||
: "border-zinc-700 text-zinc-500"
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function Section({
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
title: string
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<h2 className="mb-2 text-xs font-medium uppercase tracking-wider text-zinc-500">
|
||||
{title}
|
||||
</h2>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SdkButtons({
|
||||
sdks,
|
||||
selected,
|
||||
onSelect,
|
||||
}: {
|
||||
sdks: ChatSdkDefinition[]
|
||||
selected: string
|
||||
onSelect: (id: string) => void
|
||||
}) {
|
||||
return (
|
||||
<ul className="space-y-1">
|
||||
{sdks.map((sdk) => (
|
||||
<li key={sdk.id}>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!sdk.available}
|
||||
onClick={() => onSelect(sdk.id)}
|
||||
className={`w-full rounded-md px-2 py-2 text-left text-sm transition-colors ${
|
||||
selected === sdk.id
|
||||
? "bg-zinc-800 text-white"
|
||||
: "text-zinc-400 hover:bg-zinc-900 hover:text-zinc-200"
|
||||
} ${!sdk.available ? "opacity-40 cursor-not-allowed" : ""}`}
|
||||
>
|
||||
<div>{sdk.label}</div>
|
||||
<div className="text-[10px] text-zinc-500 capitalize">
|
||||
{sdk.mode}
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarTab({
|
||||
active,
|
||||
onClick,
|
||||
children,
|
||||
}: {
|
||||
active: boolean
|
||||
onClick: () => void
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`flex-1 rounded-md px-2 py-1.5 text-xs font-medium transition-colors ${
|
||||
active
|
||||
? "bg-zinc-800 text-white"
|
||||
: "text-zinc-500 hover:bg-zinc-900 hover:text-zinc-300"
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
140
apps/sdk-playground/src/components/ApiKeysPanel.tsx
Normal file
140
apps/sdk-playground/src/components/ApiKeysPanel.tsx
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import {
|
||||
clearStoredApiKeys,
|
||||
readStoredApiKeys,
|
||||
storeApiKeys,
|
||||
} from "@/lib/api-keys"
|
||||
|
||||
export function ApiKeysPanel({
|
||||
supermemoryApiKey,
|
||||
openaiApiKey,
|
||||
hasSupermemoryEnvKey,
|
||||
hasOpenAiEnvKey,
|
||||
onSupermemoryChange,
|
||||
onOpenAiChange,
|
||||
}: {
|
||||
supermemoryApiKey: string
|
||||
openaiApiKey: string
|
||||
hasSupermemoryEnvKey: boolean
|
||||
hasOpenAiEnvKey: boolean
|
||||
onSupermemoryChange: (value: string) => void
|
||||
onOpenAiChange: (value: string) => void
|
||||
}) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [storageInitialized, setStorageInitialized] = useState(false)
|
||||
const [rememberKeys, setRememberKeys] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const stored = readStoredApiKeys()
|
||||
const hasStoredKeys = Boolean(
|
||||
stored.supermemoryApiKey || stored.openaiApiKey,
|
||||
)
|
||||
if (stored.supermemoryApiKey) onSupermemoryChange(stored.supermemoryApiKey)
|
||||
if (stored.openaiApiKey) onOpenAiChange(stored.openaiApiKey)
|
||||
setRememberKeys(hasStoredKeys)
|
||||
setStorageInitialized(true)
|
||||
}, [onSupermemoryChange, onOpenAiChange])
|
||||
|
||||
useEffect(() => {
|
||||
if (!storageInitialized) return
|
||||
if (rememberKeys) {
|
||||
storeApiKeys({ supermemoryApiKey, openaiApiKey })
|
||||
} else {
|
||||
clearStoredApiKeys()
|
||||
}
|
||||
}, [storageInitialized, rememberKeys, supermemoryApiKey, openaiApiKey])
|
||||
|
||||
const supermemoryReady =
|
||||
supermemoryApiKey.trim().length > 0 || hasSupermemoryEnvKey
|
||||
const openAiReady = openaiApiKey.trim().length > 0 || hasOpenAiEnvKey
|
||||
const ready = supermemoryReady && openAiReady
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-zinc-800 bg-zinc-900/40">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className="flex w-full items-center justify-between gap-2 px-3 py-2 text-left text-sm"
|
||||
>
|
||||
<span className="font-medium text-zinc-200">API keys</span>
|
||||
<span className="flex items-center gap-2 text-xs">
|
||||
<span className={ready ? "text-emerald-400" : "text-amber-400"}>
|
||||
{ready ? "configured" : "required for chat"}
|
||||
</span>
|
||||
<span className="text-zinc-600">{open ? "−" : "+"}</span>
|
||||
</span>
|
||||
</button>
|
||||
{open && (
|
||||
<div className="space-y-3 border-t border-zinc-800 px-3 pb-3 pt-2">
|
||||
<p className="rounded border border-amber-900/60 bg-amber-950/30 px-2 py-1.5 text-[11px] leading-snug text-amber-200/80">
|
||||
Use disposable development or test keys only—never production
|
||||
credentials. Keys stay in this page only unless you explicitly
|
||||
enable tab-scoped storage below. Server env vars remain available as
|
||||
fallbacks.
|
||||
</p>
|
||||
<label className="block space-y-1">
|
||||
<span className="flex items-center justify-between gap-2 text-xs text-zinc-500">
|
||||
<span>Supermemory API key</span>
|
||||
{hasSupermemoryEnvKey && !supermemoryApiKey.trim() && (
|
||||
<span className="text-emerald-500">using server env</span>
|
||||
)}
|
||||
</span>
|
||||
<input
|
||||
type="password"
|
||||
value={supermemoryApiKey}
|
||||
onChange={(e) => onSupermemoryChange(e.target.value)}
|
||||
placeholder={
|
||||
hasSupermemoryEnvKey ? "Optional browser override" : "sm_…"
|
||||
}
|
||||
className="w-full rounded-md border border-zinc-700 bg-zinc-950 px-3 py-1.5 text-sm font-mono"
|
||||
/>
|
||||
</label>
|
||||
<label className="block space-y-1">
|
||||
<span className="flex items-center justify-between gap-2 text-xs text-zinc-500">
|
||||
<span>OpenAI API key</span>
|
||||
{hasOpenAiEnvKey && !openaiApiKey.trim() && (
|
||||
<span className="text-emerald-500">using server env</span>
|
||||
)}
|
||||
</span>
|
||||
<input
|
||||
type="password"
|
||||
value={openaiApiKey}
|
||||
onChange={(e) => onOpenAiChange(e.target.value)}
|
||||
placeholder={
|
||||
hasOpenAiEnvKey ? "Optional browser override" : "sk-…"
|
||||
}
|
||||
className="w-full rounded-md border border-zinc-700 bg-zinc-950 px-3 py-1.5 text-sm font-mono"
|
||||
/>
|
||||
</label>
|
||||
<label className="flex items-start gap-2 text-[11px] leading-snug text-zinc-400">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={rememberKeys}
|
||||
onChange={(event) => setRememberKeys(event.target.checked)}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<span>
|
||||
Remember these keys for this tab using sessionStorage. They are
|
||||
cleared when the tab closes; only enable this on a trusted
|
||||
profile.
|
||||
</span>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onSupermemoryChange("")
|
||||
onOpenAiChange("")
|
||||
setRememberKeys(false)
|
||||
clearStoredApiKeys()
|
||||
}}
|
||||
className="text-xs text-zinc-500 hover:text-zinc-300"
|
||||
>
|
||||
Clear entered keys
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
350
apps/sdk-playground/src/components/ContextPanel.tsx
Normal file
350
apps/sdk-playground/src/components/ContextPanel.tsx
Normal file
|
|
@ -0,0 +1,350 @@
|
|||
"use client"
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react"
|
||||
import type { ContainerContext } from "@/lib/context-api"
|
||||
|
||||
type ContextDocument = ContainerContext["documents"][number]
|
||||
|
||||
export function ContextPanel({
|
||||
containerTag,
|
||||
lastUserMessage,
|
||||
refreshKey,
|
||||
supermemoryApiKey,
|
||||
supermemoryKeyReady,
|
||||
}: {
|
||||
containerTag: string
|
||||
lastUserMessage?: string
|
||||
refreshKey: number
|
||||
supermemoryApiKey: string
|
||||
supermemoryKeyReady: boolean
|
||||
}) {
|
||||
const [context, setContext] = useState<ContainerContext | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [useQuery, setUseQuery] = useState(false)
|
||||
const [selectedDocKey, setSelectedDocKey] = useState<string | null>(null)
|
||||
const activeRequest = useRef<AbortController | null>(null)
|
||||
|
||||
const load = useCallback(async () => {
|
||||
if (!supermemoryKeyReady) {
|
||||
setError("Enter a Supermemory API key or set SUPERMEMORY_API_KEY")
|
||||
setContext(null)
|
||||
return
|
||||
}
|
||||
const normalizedContainerTag = containerTag.trim()
|
||||
if (!normalizedContainerTag) {
|
||||
setError("Enter a container tag to load context")
|
||||
setContext(null)
|
||||
return
|
||||
}
|
||||
|
||||
activeRequest.current?.abort()
|
||||
const controller = new AbortController()
|
||||
activeRequest.current = controller
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const res = await fetch("/api/context", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
signal: controller.signal,
|
||||
body: JSON.stringify({
|
||||
containerTag: normalizedContainerTag,
|
||||
...(useQuery && lastUserMessage ? { query: lastUserMessage } : {}),
|
||||
...(supermemoryApiKey.trim()
|
||||
? {
|
||||
apiKeys: {
|
||||
supermemoryApiKey: supermemoryApiKey.trim(),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
}),
|
||||
})
|
||||
const data = await res.json()
|
||||
if (!data.ok) throw new Error(data.error ?? "Failed to load context")
|
||||
if (controller.signal.aborted) return
|
||||
setContext(data.context)
|
||||
} catch (err) {
|
||||
if (err instanceof DOMException && err.name === "AbortError") return
|
||||
setError(err instanceof Error ? err.message : String(err))
|
||||
setContext(null)
|
||||
} finally {
|
||||
if (activeRequest.current === controller) {
|
||||
activeRequest.current = null
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
}, [
|
||||
containerTag,
|
||||
lastUserMessage,
|
||||
useQuery,
|
||||
supermemoryApiKey,
|
||||
supermemoryKeyReady,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
// The counter changes after a successful chat and explicitly refreshes context.
|
||||
void refreshKey
|
||||
if (!supermemoryKeyReady || !containerTag.trim()) {
|
||||
activeRequest.current?.abort()
|
||||
activeRequest.current = null
|
||||
setLoading(false)
|
||||
setContext(null)
|
||||
setError(null)
|
||||
return
|
||||
}
|
||||
|
||||
const timeout = window.setTimeout(() => {
|
||||
void load()
|
||||
}, 500)
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(timeout)
|
||||
activeRequest.current?.abort()
|
||||
}
|
||||
}, [load, refreshKey, supermemoryKeyReady, containerTag])
|
||||
|
||||
useEffect(() => {
|
||||
// A different container must not retain the previous document selection.
|
||||
void containerTag
|
||||
setSelectedDocKey(null)
|
||||
}, [containerTag])
|
||||
|
||||
const selectedDoc =
|
||||
context?.documents.find((doc) => documentKey(doc) === selectedDocKey) ??
|
||||
null
|
||||
|
||||
return (
|
||||
<div className="flex min-h-0 flex-col gap-3 text-sm">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<h2 className="text-xs font-medium uppercase tracking-wider text-zinc-500">
|
||||
Container context
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void load()}
|
||||
disabled={!supermemoryKeyReady || !containerTag.trim()}
|
||||
className="text-xs text-zinc-400 hover:text-zinc-200 disabled:cursor-not-allowed disabled:text-zinc-700"
|
||||
>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<label className="flex items-center gap-2 text-xs text-zinc-400">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={useQuery}
|
||||
onChange={(e) => setUseQuery(e.target.checked)}
|
||||
/>
|
||||
Profile with last message as query
|
||||
</label>
|
||||
|
||||
{loading && <p className="text-xs text-zinc-500">Loading…</p>}
|
||||
{!supermemoryKeyReady && (
|
||||
<p className="text-xs text-zinc-500">
|
||||
Enter a Supermemory key or set SUPERMEMORY_API_KEY to load context.
|
||||
</p>
|
||||
)}
|
||||
{error && <p className="text-xs text-red-400">{error}</p>}
|
||||
|
||||
{context && (
|
||||
<div className="min-h-0 flex-1 space-y-4 overflow-y-auto pr-1">
|
||||
<section>
|
||||
<h3 className="mb-2 text-xs font-medium text-zinc-400">
|
||||
Profile · {context.profile.static.length} static ·{" "}
|
||||
{context.profile.dynamic.length} dynamic ·{" "}
|
||||
{context.profile.searchResults.length} search
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<MemoryList title="Static" items={context.profile.static} />
|
||||
<MemoryList title="Dynamic" items={context.profile.dynamic} />
|
||||
{context.profile.searchResults.length > 0 && (
|
||||
<MemoryList
|
||||
title="Search results"
|
||||
items={context.profile.searchResults}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="min-h-0">
|
||||
<h3 className="mb-2 text-xs font-medium text-zinc-400">
|
||||
Documents / sessions ({context.documents.length})
|
||||
</h3>
|
||||
{context.documents.length === 0 ? (
|
||||
<p className="text-xs text-zinc-500">No documents yet</p>
|
||||
) : (
|
||||
<div className="flex min-h-0 gap-2">
|
||||
<ul className="min-w-0 flex-1 space-y-2">
|
||||
{context.documents.map((doc) => {
|
||||
const key = documentKey(doc)
|
||||
const isSelected = key === selectedDocKey
|
||||
const memoryCount = doc.memoryEntries?.length ?? 0
|
||||
return (
|
||||
<li key={key}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setSelectedDocKey(isSelected ? null : key)
|
||||
}
|
||||
className={`w-full rounded border p-2 text-left text-xs transition-colors ${
|
||||
isSelected
|
||||
? "border-emerald-700/60 bg-emerald-950/30"
|
||||
: "border-zinc-800 bg-zinc-900/50 hover:border-zinc-700"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0 font-mono text-zinc-300 truncate">
|
||||
{doc.id ?? "—"}
|
||||
</div>
|
||||
<span
|
||||
className={`shrink-0 rounded-full px-1.5 py-0.5 text-[10px] font-medium tabular-nums ${
|
||||
memoryCount > 0
|
||||
? "bg-emerald-900/50 text-emerald-300"
|
||||
: "bg-zinc-800 text-zinc-500"
|
||||
}`}
|
||||
>
|
||||
{memoryCount}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-zinc-500 truncate">
|
||||
{doc.title ?? "untitled"}
|
||||
</div>
|
||||
<div className="text-zinc-600">
|
||||
{doc.customId
|
||||
? `session: ${doc.customId}`
|
||||
: "no customId"}
|
||||
{doc.status ? ` · ${doc.status}` : ""}
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
|
||||
{selectedDoc && (
|
||||
<div className="min-w-0 flex-1 border-l border-zinc-800 pl-2">
|
||||
<DocumentMemoriesPanel doc={selectedDoc} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{context.documents.length > 0 && !selectedDoc && (
|
||||
<p className="mt-2 text-[10px] text-zinc-600">
|
||||
Click a document to view its memories
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function DocumentMemoriesPanel({ doc }: { doc: ContextDocument }) {
|
||||
const entries = doc.memoryEntries ?? []
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="text-[10px] uppercase tracking-wide text-zinc-600">
|
||||
Document memories ({entries.length})
|
||||
</div>
|
||||
<div className="text-xs text-zinc-500 truncate">
|
||||
{doc.title ?? "untitled"}
|
||||
</div>
|
||||
{doc.summary && (
|
||||
<p className="text-[11px] leading-snug text-zinc-500 line-clamp-3">
|
||||
{doc.summary}
|
||||
</p>
|
||||
)}
|
||||
{entries.length === 0 ? (
|
||||
<p className="text-xs text-zinc-600">No memories on this document</p>
|
||||
) : (
|
||||
<ul className="space-y-2 max-h-64 overflow-y-auto pr-1">
|
||||
{entries.map((entry, i) => (
|
||||
<li
|
||||
key={memoryEntryKey(entry, i)}
|
||||
className="rounded border border-zinc-800 bg-zinc-900/60 p-2"
|
||||
>
|
||||
<MemoryEntryCard entry={entry} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function MemoryEntryCard({ entry }: { entry: unknown }) {
|
||||
const record =
|
||||
entry && typeof entry === "object"
|
||||
? (entry as Record<string, unknown>)
|
||||
: null
|
||||
|
||||
const memoryText = formatMemoryItem(entry)
|
||||
const id = record?.id as string | undefined
|
||||
const version = record?.version as number | undefined
|
||||
const isForgotten = Boolean(record?.isForgotten)
|
||||
const isStatic = Boolean(record?.isStatic)
|
||||
const forgetAfter = record?.forgetAfter as string | undefined
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{memoryText && (
|
||||
<p className="text-xs leading-snug text-zinc-300">{memoryText}</p>
|
||||
)}
|
||||
<div className="flex flex-wrap gap-1 text-[10px] text-zinc-600">
|
||||
{id && <span className="font-mono truncate max-w-full">{id}</span>}
|
||||
{version != null && <span>v{version}</span>}
|
||||
{isStatic && <span className="text-sky-500">static</span>}
|
||||
{isForgotten && <span className="text-amber-500">forgotten</span>}
|
||||
{forgetAfter && !isForgotten && (
|
||||
<span className="text-orange-500">expires</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function documentKey(doc: ContextDocument): string {
|
||||
return doc.id ?? doc.customId ?? doc.title ?? "unknown"
|
||||
}
|
||||
|
||||
function memoryEntryKey(entry: unknown, index: number): string {
|
||||
if (entry && typeof entry === "object" && "id" in entry) {
|
||||
return String((entry as { id: unknown }).id)
|
||||
}
|
||||
return `memory-${index}`
|
||||
}
|
||||
|
||||
function MemoryList({ title, items }: { title: string; items: unknown[] }) {
|
||||
if (!items.length) return null
|
||||
return (
|
||||
<div>
|
||||
<div className="text-[10px] uppercase tracking-wide text-zinc-600 mb-1">
|
||||
{title}
|
||||
</div>
|
||||
<ul className="space-y-1">
|
||||
{items.slice(0, 12).map((item, i) => (
|
||||
<li
|
||||
key={i}
|
||||
className="rounded bg-zinc-900/60 px-2 py-1 text-xs text-zinc-300 leading-snug"
|
||||
>
|
||||
{formatMemoryItem(item)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function formatMemoryItem(item: unknown): string {
|
||||
if (typeof item === "string") return item
|
||||
if (item && typeof item === "object") {
|
||||
const record = item as Record<string, unknown>
|
||||
if (typeof record.memory === "string") return record.memory
|
||||
if (typeof record.content === "string") return record.content
|
||||
if (typeof record.chunk === "string") return record.chunk
|
||||
}
|
||||
return JSON.stringify(item)
|
||||
}
|
||||
122
apps/sdk-playground/src/components/ToolsReferencePanel.tsx
Normal file
122
apps/sdk-playground/src/components/ToolsReferencePanel.tsx
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { TOOL_CATALOG, type CatalogTool } from "@/lib/tools-catalog"
|
||||
|
||||
export function ToolsReferencePanel() {
|
||||
const [expandedId, setExpandedId] = useState<string | null>("documentAdd")
|
||||
|
||||
return (
|
||||
<div className="flex min-h-0 flex-col gap-2 text-sm">
|
||||
<div>
|
||||
<h2 className="text-xs font-medium uppercase tracking-wider text-zinc-500">
|
||||
Tool reference
|
||||
</h2>
|
||||
<p className="mt-1 text-[10px] leading-snug text-zinc-600">
|
||||
Canonical descriptions from{" "}
|
||||
<code className="text-zinc-500">@supermemory/tools</code> — what the
|
||||
model sees in tools mode.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ul className="min-h-0 flex-1 space-y-2 overflow-y-auto pr-1">
|
||||
{TOOL_CATALOG.map((tool) => (
|
||||
<ToolCard
|
||||
key={tool.id}
|
||||
tool={tool}
|
||||
expanded={expandedId === tool.id}
|
||||
onToggle={() =>
|
||||
setExpandedId((id) => (id === tool.id ? null : tool.id))
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ToolCard({
|
||||
tool,
|
||||
expanded,
|
||||
onToggle,
|
||||
}: {
|
||||
tool: CatalogTool
|
||||
expanded: boolean
|
||||
onToggle: () => void
|
||||
}) {
|
||||
return (
|
||||
<li className="rounded border border-zinc-800 bg-zinc-900/40">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggle}
|
||||
className="w-full px-2 py-2 text-left"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<div className="font-mono text-xs text-emerald-400/90">
|
||||
{tool.id}
|
||||
</div>
|
||||
<div className="font-mono text-[10px] text-zinc-600">
|
||||
py: {tool.pythonName}
|
||||
</div>
|
||||
</div>
|
||||
<span className="shrink-0 text-[10px] text-zinc-600">
|
||||
{expanded ? "−" : "+"}
|
||||
</span>
|
||||
</div>
|
||||
{!expanded && (
|
||||
<p className="mt-1 line-clamp-2 text-[11px] leading-snug text-zinc-500">
|
||||
{tool.description}
|
||||
</p>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{expanded && (
|
||||
<div className="border-t border-zinc-800 px-2 pb-2 pt-1 space-y-2">
|
||||
<p className="text-[11px] leading-relaxed text-zinc-300">
|
||||
{tool.description}
|
||||
</p>
|
||||
{tool.parameters.length > 0 && (
|
||||
<div>
|
||||
<div className="text-[10px] uppercase tracking-wide text-zinc-600 mb-1">
|
||||
Parameters
|
||||
</div>
|
||||
<ul className="space-y-1.5">
|
||||
{tool.parameters.map((param) => (
|
||||
<li
|
||||
key={param.name}
|
||||
className="rounded bg-zinc-950/60 px-2 py-1"
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
<span className="font-mono text-[10px] text-sky-400/90">
|
||||
{param.name}
|
||||
</span>
|
||||
{param.pythonName && param.pythonName !== param.name && (
|
||||
<span className="font-mono text-[10px] text-zinc-600">
|
||||
/ {param.pythonName}
|
||||
</span>
|
||||
)}
|
||||
{!param.pythonName && (
|
||||
<span className="text-[9px] text-sky-500/80">
|
||||
TypeScript only
|
||||
</span>
|
||||
)}
|
||||
{param.required && (
|
||||
<span className="text-[9px] text-amber-500/80">
|
||||
required
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-0.5 text-[10px] leading-snug text-zinc-500">
|
||||
{param.description}
|
||||
</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
67
apps/sdk-playground/src/lib/api-keys.ts
Normal file
67
apps/sdk-playground/src/lib/api-keys.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
export interface PlaygroundApiKeys {
|
||||
supermemoryApiKey: string
|
||||
openaiApiKey: string
|
||||
}
|
||||
|
||||
export const API_KEYS_STORAGE_KEY = "sdk-playground-api-keys"
|
||||
|
||||
interface ResolveApiKeyOptions {
|
||||
allowEnvironment?: boolean
|
||||
}
|
||||
|
||||
export function resolveSupermemoryApiKey(
|
||||
input?: Partial<PlaygroundApiKeys> | null,
|
||||
options: ResolveApiKeyOptions = {},
|
||||
): string | null {
|
||||
const provided = input?.supermemoryApiKey?.trim()
|
||||
if (provided) return provided
|
||||
if (options.allowEnvironment === false) return null
|
||||
return process.env.SUPERMEMORY_API_KEY?.trim() || null
|
||||
}
|
||||
|
||||
export function resolveOpenAiApiKey(
|
||||
input?: Partial<PlaygroundApiKeys> | null,
|
||||
options: ResolveApiKeyOptions = {},
|
||||
): string | null {
|
||||
const provided = input?.openaiApiKey?.trim()
|
||||
if (provided) return provided
|
||||
if (options.allowEnvironment === false) return null
|
||||
return process.env.OPENAI_API_KEY?.trim() || null
|
||||
}
|
||||
|
||||
export function resolveApiKeys(
|
||||
input?: Partial<PlaygroundApiKeys> | null,
|
||||
options: ResolveApiKeyOptions = {},
|
||||
): PlaygroundApiKeys | null {
|
||||
const supermemoryApiKey = resolveSupermemoryApiKey(input, options)
|
||||
const openaiApiKey = resolveOpenAiApiKey(input, options)
|
||||
|
||||
if (!supermemoryApiKey || !openaiApiKey) return null
|
||||
|
||||
return { supermemoryApiKey, openaiApiKey }
|
||||
}
|
||||
|
||||
export function readStoredApiKeys(): Partial<PlaygroundApiKeys> {
|
||||
if (typeof window === "undefined") return {}
|
||||
try {
|
||||
const raw = sessionStorage.getItem(API_KEYS_STORAGE_KEY)
|
||||
if (!raw) return {}
|
||||
const parsed = JSON.parse(raw) as Partial<PlaygroundApiKeys>
|
||||
return {
|
||||
supermemoryApiKey: parsed.supermemoryApiKey ?? "",
|
||||
openaiApiKey: parsed.openaiApiKey ?? "",
|
||||
}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
export function storeApiKeys(keys: Partial<PlaygroundApiKeys>) {
|
||||
if (typeof window === "undefined") return
|
||||
sessionStorage.setItem(API_KEYS_STORAGE_KEY, JSON.stringify(keys))
|
||||
}
|
||||
|
||||
export function clearStoredApiKeys() {
|
||||
if (typeof window === "undefined") return
|
||||
sessionStorage.removeItem(API_KEYS_STORAGE_KEY)
|
||||
}
|
||||
437
apps/sdk-playground/src/lib/chat-handlers.ts
Normal file
437
apps/sdk-playground/src/lib/chat-handlers.ts
Normal file
|
|
@ -0,0 +1,437 @@
|
|||
import { createOpenAI } from "@ai-sdk/openai"
|
||||
import { generateText, stepCountIs, type ModelMessage } from "ai"
|
||||
import OpenAI from "openai"
|
||||
import { supermemoryTools as aiSdkPackageTools } from "@supermemory/ai-sdk"
|
||||
import { withSupermemory as withSupermemoryAiSdk } from "@supermemory/tools/ai-sdk"
|
||||
import {
|
||||
createToolCallsExecutor,
|
||||
getToolDefinitions,
|
||||
withSupermemory as withSupermemoryOpenAi,
|
||||
} from "@supermemory/tools/openai"
|
||||
import { supermemoryTools as aiSdkTools } from "@supermemory/tools/ai-sdk"
|
||||
import type { SupermemoryToolsConfig } from "@supermemory/tools"
|
||||
import type { PlaygroundApiKeys } from "./api-keys"
|
||||
import {
|
||||
buildMiddlewareMemoryDebug,
|
||||
type MemoryDebugEntry,
|
||||
} from "./context-api"
|
||||
import {
|
||||
normalizeMiddlewareConfig,
|
||||
type MiddlewareRuntimeConfig,
|
||||
} from "./middleware-config"
|
||||
import {
|
||||
TOOLS_SYSTEM_PROMPT,
|
||||
getChatSdk,
|
||||
type ToolTraceEntry,
|
||||
} from "./sdk-registry"
|
||||
|
||||
export type ChatMessage = {
|
||||
role: "user" | "assistant" | "system"
|
||||
content: string
|
||||
}
|
||||
|
||||
export interface ChatResult {
|
||||
text: string
|
||||
toolTrace: ToolTraceEntry[]
|
||||
memoryDebug: MemoryDebugEntry[]
|
||||
}
|
||||
|
||||
export interface ChatRequest {
|
||||
sdkId: string
|
||||
messages: ChatMessage[]
|
||||
containerTag: string
|
||||
conversationId: string
|
||||
memoryMode?: "profile" | "query" | "full"
|
||||
middlewareConfig?: Partial<MiddlewareRuntimeConfig>
|
||||
apiKeys?: Partial<PlaygroundApiKeys>
|
||||
containerTags?: string[]
|
||||
projectId?: string
|
||||
}
|
||||
|
||||
export class PlaygroundChatTimeoutError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message)
|
||||
this.name = "PlaygroundChatTimeoutError"
|
||||
}
|
||||
}
|
||||
|
||||
const MODEL_REQUEST_TIMEOUT_MS = 120_000
|
||||
const DEBUG_REQUEST_TIMEOUT_MS = 10_000
|
||||
const MAX_OUTPUT_TOKENS = 2_048
|
||||
|
||||
async function withChatDeadline<T>(
|
||||
operation: (signal: AbortSignal) => Promise<T>,
|
||||
): Promise<T> {
|
||||
const controller = new AbortController()
|
||||
let timeout: ReturnType<typeof setTimeout> | undefined
|
||||
const deadline = new Promise<never>((_resolve, reject) => {
|
||||
timeout = setTimeout(() => {
|
||||
const error = new PlaygroundChatTimeoutError(
|
||||
`TypeScript chat timed out after ${MODEL_REQUEST_TIMEOUT_MS / 1_000} seconds`,
|
||||
)
|
||||
controller.abort(error)
|
||||
reject(error)
|
||||
}, MODEL_REQUEST_TIMEOUT_MS)
|
||||
})
|
||||
|
||||
try {
|
||||
return await Promise.race([operation(controller.signal), deadline])
|
||||
} finally {
|
||||
if (timeout) clearTimeout(timeout)
|
||||
}
|
||||
}
|
||||
|
||||
async function buildBestEffortDebug(
|
||||
operation: (signal: AbortSignal) => Promise<MemoryDebugEntry[]>,
|
||||
): Promise<MemoryDebugEntry[]> {
|
||||
const controller = new AbortController()
|
||||
let timeout: ReturnType<typeof setTimeout> | undefined
|
||||
const deadline = new Promise<MemoryDebugEntry[]>((resolve) => {
|
||||
timeout = setTimeout(() => {
|
||||
controller.abort()
|
||||
resolve([
|
||||
{
|
||||
type: "debug_error",
|
||||
label: "Post-response context reconstruction timed out",
|
||||
detail: { nonFatal: true },
|
||||
},
|
||||
])
|
||||
}, DEBUG_REQUEST_TIMEOUT_MS)
|
||||
})
|
||||
|
||||
try {
|
||||
return await Promise.race([operation(controller.signal), deadline])
|
||||
} catch {
|
||||
return [
|
||||
{
|
||||
type: "debug_error",
|
||||
label: "Post-response context reconstruction unavailable",
|
||||
detail: { nonFatal: true },
|
||||
},
|
||||
]
|
||||
} finally {
|
||||
if (timeout) clearTimeout(timeout)
|
||||
}
|
||||
}
|
||||
|
||||
function getModelName(): string {
|
||||
return process.env.MODEL_NAME ?? "gpt-4o-mini"
|
||||
}
|
||||
|
||||
function getToolsConfig(
|
||||
containerTags?: string[],
|
||||
projectId?: string,
|
||||
): SupermemoryToolsConfig {
|
||||
return {
|
||||
baseUrl: process.env.SUPERMEMORY_BASE_URL,
|
||||
...(containerTags?.length ? { containerTags } : {}),
|
||||
...(projectId ? { projectId } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
function toModelMessages(messages: ChatMessage[]): ModelMessage[] {
|
||||
return messages.map((m) => ({ role: m.role, content: m.content }))
|
||||
}
|
||||
|
||||
function toOpenAiMessages(
|
||||
messages: ChatMessage[],
|
||||
): OpenAI.Chat.Completions.ChatCompletionMessageParam[] {
|
||||
return messages.map((m) => ({ role: m.role, content: m.content }))
|
||||
}
|
||||
|
||||
function extractAiSdkToolTrace(
|
||||
steps: Array<{
|
||||
toolCalls: Array<{ toolName: string; input: unknown }>
|
||||
toolResults: Array<{ toolName: string; output: unknown }>
|
||||
}>,
|
||||
): ToolTraceEntry[] {
|
||||
const trace: ToolTraceEntry[] = []
|
||||
for (const [stepIndex, step] of steps.entries()) {
|
||||
for (let i = 0; i < step.toolCalls.length; i++) {
|
||||
const call = step.toolCalls[i]
|
||||
const result = step.toolResults[i]
|
||||
trace.push({
|
||||
step: stepIndex + 1,
|
||||
toolName: call.toolName,
|
||||
args: call.input,
|
||||
result: result?.output,
|
||||
})
|
||||
}
|
||||
}
|
||||
return trace
|
||||
}
|
||||
|
||||
function lastUserMessage(messages: ChatMessage[]): string {
|
||||
return [...messages].reverse().find((m) => m.role === "user")?.content ?? ""
|
||||
}
|
||||
|
||||
async function chatAiSdkMiddleware(
|
||||
keys: PlaygroundApiKeys,
|
||||
messages: ChatMessage[],
|
||||
containerTag: string,
|
||||
conversationId: string,
|
||||
memoryMode: "profile" | "query" | "full",
|
||||
middlewareConfig: MiddlewareRuntimeConfig,
|
||||
signal: AbortSignal,
|
||||
): Promise<ChatResult> {
|
||||
const openai = createOpenAI({ apiKey: keys.openaiApiKey })
|
||||
const model = withSupermemoryAiSdk(openai(getModelName()), {
|
||||
containerTag,
|
||||
customId: conversationId,
|
||||
apiKey: keys.supermemoryApiKey,
|
||||
mode: memoryMode,
|
||||
addMemory: middlewareConfig.addMemory,
|
||||
verbose: middlewareConfig.verbose,
|
||||
includeToolCalls: middlewareConfig.includeToolCalls,
|
||||
skipMemoryOnError: middlewareConfig.skipMemoryOnError,
|
||||
baseUrl: process.env.SUPERMEMORY_BASE_URL,
|
||||
})
|
||||
|
||||
const result = await generateText({
|
||||
model,
|
||||
system: "You are a helpful assistant with long-term memory about the user.",
|
||||
messages: toModelMessages(messages.filter((m) => m.role !== "system")),
|
||||
maxOutputTokens: MAX_OUTPUT_TOKENS,
|
||||
abortSignal: signal,
|
||||
})
|
||||
|
||||
return { text: result.text, toolTrace: [], memoryDebug: [] }
|
||||
}
|
||||
|
||||
async function chatOpenAiMiddleware(
|
||||
keys: PlaygroundApiKeys,
|
||||
messages: ChatMessage[],
|
||||
containerTag: string,
|
||||
conversationId: string,
|
||||
memoryMode: "profile" | "query" | "full",
|
||||
middlewareConfig: MiddlewareRuntimeConfig,
|
||||
signal: AbortSignal,
|
||||
): Promise<ChatResult> {
|
||||
const openai = new OpenAI({
|
||||
apiKey: keys.openaiApiKey,
|
||||
timeout: MODEL_REQUEST_TIMEOUT_MS,
|
||||
maxRetries: 1,
|
||||
})
|
||||
const client = withSupermemoryOpenAi(openai, {
|
||||
containerTag,
|
||||
customId: conversationId,
|
||||
apiKey: keys.supermemoryApiKey,
|
||||
mode: memoryMode,
|
||||
addMemory: middlewareConfig.addMemory,
|
||||
verbose: middlewareConfig.verbose,
|
||||
baseUrl: process.env.SUPERMEMORY_BASE_URL,
|
||||
})
|
||||
|
||||
const response = await client.chat.completions.create(
|
||||
{
|
||||
model: getModelName(),
|
||||
messages: toOpenAiMessages(messages),
|
||||
max_tokens: MAX_OUTPUT_TOKENS,
|
||||
},
|
||||
{ signal },
|
||||
)
|
||||
|
||||
return {
|
||||
text: response.choices[0]?.message?.content ?? "",
|
||||
toolTrace: [],
|
||||
memoryDebug: [],
|
||||
}
|
||||
}
|
||||
|
||||
async function chatAiSdkTools(
|
||||
keys: PlaygroundApiKeys,
|
||||
toolsFactory: typeof aiSdkTools,
|
||||
messages: ChatMessage[],
|
||||
containerTags?: string[],
|
||||
projectId?: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<ChatResult> {
|
||||
const openai = createOpenAI({ apiKey: keys.openaiApiKey })
|
||||
const tools = toolsFactory(
|
||||
keys.supermemoryApiKey,
|
||||
getToolsConfig(containerTags, projectId),
|
||||
)
|
||||
|
||||
const result = await generateText({
|
||||
model: openai(getModelName()),
|
||||
system: TOOLS_SYSTEM_PROMPT,
|
||||
messages: toModelMessages(messages.filter((m) => m.role !== "system")),
|
||||
tools,
|
||||
stopWhen: stepCountIs(8),
|
||||
maxOutputTokens: MAX_OUTPUT_TOKENS,
|
||||
abortSignal: signal,
|
||||
})
|
||||
|
||||
return {
|
||||
text: result.text,
|
||||
toolTrace: extractAiSdkToolTrace(result.steps),
|
||||
memoryDebug: [],
|
||||
}
|
||||
}
|
||||
|
||||
async function chatOpenAiTools(
|
||||
keys: PlaygroundApiKeys,
|
||||
messages: ChatMessage[],
|
||||
containerTags?: string[],
|
||||
projectId?: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<ChatResult> {
|
||||
const openai = new OpenAI({
|
||||
apiKey: keys.openaiApiKey,
|
||||
timeout: MODEL_REQUEST_TIMEOUT_MS,
|
||||
maxRetries: 1,
|
||||
})
|
||||
const config = getToolsConfig(containerTags, projectId)
|
||||
const executeToolCalls = createToolCallsExecutor(
|
||||
keys.supermemoryApiKey,
|
||||
config,
|
||||
)
|
||||
const toolDefs = getToolDefinitions()
|
||||
const trace: ToolTraceEntry[] = []
|
||||
|
||||
const convo: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: TOOLS_SYSTEM_PROMPT },
|
||||
...toOpenAiMessages(messages.filter((m) => m.role !== "system")),
|
||||
]
|
||||
|
||||
for (let step = 0; step < 8; step++) {
|
||||
const response = await openai.chat.completions.create(
|
||||
{
|
||||
model: getModelName(),
|
||||
messages: convo,
|
||||
tools: toolDefs,
|
||||
max_tokens: MAX_OUTPUT_TOKENS,
|
||||
},
|
||||
{ signal },
|
||||
)
|
||||
|
||||
const choice = response.choices[0]?.message
|
||||
if (!choice) break
|
||||
|
||||
convo.push(choice)
|
||||
|
||||
if (choice.tool_calls?.length) {
|
||||
const toolMessages = await executeToolCalls(choice.tool_calls)
|
||||
for (let i = 0; i < choice.tool_calls.length; i++) {
|
||||
const call = choice.tool_calls[i]
|
||||
const rawContent = toolMessages[i]?.content
|
||||
const raw =
|
||||
typeof rawContent === "string"
|
||||
? rawContent
|
||||
: JSON.stringify(rawContent)
|
||||
let parsedResult: unknown = raw
|
||||
try {
|
||||
parsedResult = JSON.parse(raw)
|
||||
} catch {
|
||||
/* keep string */
|
||||
}
|
||||
trace.push({
|
||||
step: step + 1,
|
||||
toolName: call.function.name,
|
||||
args: JSON.parse(call.function.arguments),
|
||||
result: parsedResult,
|
||||
})
|
||||
}
|
||||
convo.push(...toolMessages)
|
||||
continue
|
||||
}
|
||||
|
||||
return { text: choice.content ?? "", toolTrace: trace, memoryDebug: [] }
|
||||
}
|
||||
|
||||
throw new Error("Tool loop exceeded max steps")
|
||||
}
|
||||
|
||||
export async function runTypeScriptChat(
|
||||
request: ChatRequest,
|
||||
keys: PlaygroundApiKeys,
|
||||
): Promise<ChatResult> {
|
||||
const sdk = getChatSdk(request.sdkId)
|
||||
if (!sdk || sdk.language !== "typescript" || !sdk.available) {
|
||||
throw new Error(`Invalid TypeScript chat SDK: ${request.sdkId}`)
|
||||
}
|
||||
|
||||
const containerTags = request.containerTags ?? [request.containerTag]
|
||||
const memoryMode = request.memoryMode ?? "full"
|
||||
const middlewareConfig = normalizeMiddlewareConfig(request.middlewareConfig)
|
||||
|
||||
const result = await withChatDeadline(async (signal) => {
|
||||
switch (request.sdkId) {
|
||||
case "ts-ai-sdk-middleware":
|
||||
return await chatAiSdkMiddleware(
|
||||
keys,
|
||||
request.messages,
|
||||
request.containerTag,
|
||||
request.conversationId,
|
||||
memoryMode,
|
||||
middlewareConfig,
|
||||
signal,
|
||||
)
|
||||
case "ts-openai-middleware":
|
||||
return await chatOpenAiMiddleware(
|
||||
keys,
|
||||
request.messages,
|
||||
request.containerTag,
|
||||
request.conversationId,
|
||||
memoryMode,
|
||||
middlewareConfig,
|
||||
signal,
|
||||
)
|
||||
case "ts-ai-sdk-tools":
|
||||
return await chatAiSdkTools(
|
||||
keys,
|
||||
aiSdkTools,
|
||||
request.messages,
|
||||
containerTags,
|
||||
request.projectId,
|
||||
signal,
|
||||
)
|
||||
case "ts-openai-tools":
|
||||
return await chatOpenAiTools(
|
||||
keys,
|
||||
request.messages,
|
||||
containerTags,
|
||||
request.projectId,
|
||||
signal,
|
||||
)
|
||||
case "ts-ai-sdk-package":
|
||||
return await chatAiSdkTools(
|
||||
keys,
|
||||
aiSdkPackageTools,
|
||||
request.messages,
|
||||
containerTags,
|
||||
request.projectId,
|
||||
signal,
|
||||
)
|
||||
default:
|
||||
throw new Error(`Unhandled SDK: ${request.sdkId}`)
|
||||
}
|
||||
})
|
||||
|
||||
if (
|
||||
request.sdkId !== "ts-ai-sdk-middleware" &&
|
||||
request.sdkId !== "ts-openai-middleware"
|
||||
) {
|
||||
return result
|
||||
}
|
||||
|
||||
const memoryDebug = await buildBestEffortDebug((signal) =>
|
||||
buildMiddlewareMemoryDebug(
|
||||
request.containerTag,
|
||||
request.conversationId,
|
||||
memoryMode,
|
||||
lastUserMessage(request.messages),
|
||||
middlewareConfig,
|
||||
request.sdkId === "ts-ai-sdk-middleware"
|
||||
? {
|
||||
includeToolCalls: middlewareConfig.includeToolCalls,
|
||||
skipMemoryOnError: middlewareConfig.skipMemoryOnError,
|
||||
}
|
||||
: undefined,
|
||||
keys.supermemoryApiKey,
|
||||
signal,
|
||||
),
|
||||
)
|
||||
|
||||
return { ...result, memoryDebug }
|
||||
}
|
||||
313
apps/sdk-playground/src/lib/context-api.ts
Normal file
313
apps/sdk-playground/src/lib/context-api.ts
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
import Supermemory from "supermemory"
|
||||
import {
|
||||
type MiddlewareRuntimeConfig,
|
||||
normalizeMiddlewareConfig,
|
||||
} from "./middleware-config"
|
||||
|
||||
export interface MemoryDebugEntry {
|
||||
type:
|
||||
| "context_reconstruction"
|
||||
| "context_preview"
|
||||
| "conversation_save_requested"
|
||||
| "conversation_save_accepted"
|
||||
| "conversation_save_failed"
|
||||
| "conversation_save_queued"
|
||||
| "conversation_save_skipped"
|
||||
| "conversation_saved"
|
||||
| "profile_fetch"
|
||||
| "context_debug_unavailable"
|
||||
| "debug_error"
|
||||
| "manual_profile"
|
||||
label: string
|
||||
detail?: Record<string, unknown>
|
||||
preview?: string
|
||||
}
|
||||
|
||||
export interface ContainerContext {
|
||||
containerTag: string
|
||||
query?: string
|
||||
profile: {
|
||||
static: unknown[]
|
||||
dynamic: unknown[]
|
||||
searchResults: unknown[]
|
||||
}
|
||||
documents: Array<{
|
||||
id?: string
|
||||
title?: string
|
||||
status?: string
|
||||
customId?: string
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
summary?: string
|
||||
memoryEntries?: unknown[]
|
||||
}>
|
||||
pagination?: unknown
|
||||
}
|
||||
|
||||
function getSupermemoryClient(apiKey: string) {
|
||||
if (!apiKey) throw new Error("Supermemory API key is required")
|
||||
return new Supermemory({
|
||||
apiKey,
|
||||
timeout: 10_000,
|
||||
maxRetries: 1,
|
||||
...(process.env.SUPERMEMORY_BASE_URL
|
||||
? { baseURL: process.env.SUPERMEMORY_BASE_URL }
|
||||
: {}),
|
||||
})
|
||||
}
|
||||
|
||||
function normalizeMemoryEntries(record: Record<string, unknown>): unknown[] {
|
||||
const raw =
|
||||
record.memoryEntries ??
|
||||
record.memory_entries ??
|
||||
(Array.isArray(record.memories) &&
|
||||
record.memories.length > 0 &&
|
||||
typeof (record.memories[0] as Record<string, unknown>)?.memory === "string"
|
||||
? record.memories
|
||||
: undefined)
|
||||
|
||||
return Array.isArray(raw) ? raw : []
|
||||
}
|
||||
|
||||
function memoryText(item: unknown): string {
|
||||
if (typeof item === "string") return item
|
||||
if (item && typeof item === "object") {
|
||||
const record = item as Record<string, unknown>
|
||||
if (typeof record.memory === "string") return record.memory
|
||||
if (typeof record.content === "string") return record.content
|
||||
if (typeof record.chunk === "string") return record.chunk
|
||||
}
|
||||
return JSON.stringify(item)
|
||||
}
|
||||
|
||||
function summarizeProfile(profile: ContainerContext["profile"]) {
|
||||
return {
|
||||
staticCount: profile.static.length,
|
||||
dynamicCount: profile.dynamic.length,
|
||||
searchResultCount: profile.searchResults.length,
|
||||
staticPreview: profile.static.slice(0, 5).map(memoryText),
|
||||
dynamicPreview: profile.dynamic.slice(0, 5).map(memoryText),
|
||||
searchPreview: profile.searchResults.slice(0, 5).map(memoryText),
|
||||
}
|
||||
}
|
||||
|
||||
function selectProfileForMode(
|
||||
profile: ContainerContext["profile"],
|
||||
mode: "profile" | "query" | "full",
|
||||
): ContainerContext["profile"] {
|
||||
return {
|
||||
static: mode === "query" ? [] : profile.static,
|
||||
dynamic: mode === "query" ? [] : profile.dynamic,
|
||||
searchResults: mode === "profile" ? [] : profile.searchResults,
|
||||
}
|
||||
}
|
||||
|
||||
function buildContextPreview(
|
||||
profile: ContainerContext["profile"],
|
||||
mode: "profile" | "query" | "full",
|
||||
query?: string,
|
||||
): string {
|
||||
const lines: string[] = [`[memory mode: ${mode}]`]
|
||||
if (query) lines.push(`[query: ${query}]`)
|
||||
if (mode !== "query" && profile.static.length) {
|
||||
lines.push("Static:")
|
||||
for (const item of profile.static.slice(0, 8)) {
|
||||
lines.push(`- ${memoryText(item)}`)
|
||||
}
|
||||
}
|
||||
if (mode !== "query" && profile.dynamic.length) {
|
||||
lines.push("Dynamic:")
|
||||
for (const item of profile.dynamic.slice(0, 8)) {
|
||||
lines.push(`- ${memoryText(item)}`)
|
||||
}
|
||||
}
|
||||
if (mode !== "profile" && profile.searchResults.length) {
|
||||
lines.push("Search results:")
|
||||
for (const item of profile.searchResults.slice(0, 8)) {
|
||||
lines.push(`- ${memoryText(item)}`)
|
||||
}
|
||||
}
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
function normalizeSearchResults(searchResults: unknown): unknown[] {
|
||||
if (!searchResults) return []
|
||||
if (Array.isArray(searchResults)) return searchResults
|
||||
if (typeof searchResults === "object") {
|
||||
const record = searchResults as Record<string, unknown>
|
||||
if (Array.isArray(record.results)) return record.results
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
export function resolveProfileQuery(
|
||||
lastUserMessage: string,
|
||||
mode: "profile" | "query" | "full",
|
||||
): string | undefined {
|
||||
if (mode === "profile") return undefined
|
||||
return lastUserMessage || undefined
|
||||
}
|
||||
|
||||
async function fetchProfileContext(
|
||||
client: ReturnType<typeof getSupermemoryClient>,
|
||||
containerTag: string,
|
||||
query?: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<ContainerContext["profile"]> {
|
||||
const profileResponse = await client.profile(
|
||||
{
|
||||
containerTag,
|
||||
...(query ? { q: query } : {}),
|
||||
},
|
||||
{ signal },
|
||||
)
|
||||
const profileRaw = profileResponse.profile as
|
||||
| { static?: unknown[]; dynamic?: unknown[] }
|
||||
| undefined
|
||||
|
||||
return {
|
||||
static: profileRaw?.static ?? [],
|
||||
dynamic: profileRaw?.dynamic ?? [],
|
||||
searchResults: normalizeSearchResults(profileResponse.searchResults),
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchContainerContext(
|
||||
containerTag: string,
|
||||
query?: string,
|
||||
supermemoryApiKey?: string,
|
||||
): Promise<ContainerContext> {
|
||||
const apiKey =
|
||||
supermemoryApiKey?.trim() || process.env.SUPERMEMORY_API_KEY?.trim()
|
||||
if (!apiKey) throw new Error("Supermemory API key is required")
|
||||
|
||||
const client = getSupermemoryClient(apiKey)
|
||||
const profile = await fetchProfileContext(client, containerTag, query)
|
||||
|
||||
const docsResponse = await client.post<{
|
||||
documents?: unknown[]
|
||||
pagination?: unknown
|
||||
}>("/v3/documents/documents", {
|
||||
body: {
|
||||
containerTags: [containerTag],
|
||||
limit: 25,
|
||||
sort: "createdAt",
|
||||
order: "desc",
|
||||
},
|
||||
})
|
||||
|
||||
const rawDocuments = docsResponse.documents ?? []
|
||||
const documents = rawDocuments.map((doc) => {
|
||||
const record = doc as Record<string, unknown>
|
||||
return {
|
||||
id: record.id as string | undefined,
|
||||
title: record.title as string | undefined,
|
||||
status: record.status as string | undefined,
|
||||
customId: record.customId as string | undefined,
|
||||
createdAt: record.createdAt as string | undefined,
|
||||
updatedAt: record.updatedAt as string | undefined,
|
||||
summary: record.summary as string | undefined,
|
||||
memoryEntries: normalizeMemoryEntries(record),
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
containerTag,
|
||||
query,
|
||||
profile,
|
||||
documents,
|
||||
pagination: docsResponse.pagination,
|
||||
}
|
||||
}
|
||||
|
||||
export async function buildMiddlewareMemoryDebug(
|
||||
containerTag: string,
|
||||
conversationId: string,
|
||||
memoryMode: "profile" | "query" | "full",
|
||||
lastUserMessage: string,
|
||||
middlewareConfig?: Partial<MiddlewareRuntimeConfig>,
|
||||
aiSdkExtras?: {
|
||||
includeToolCalls?: boolean
|
||||
skipMemoryOnError?: boolean
|
||||
},
|
||||
supermemoryApiKey?: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<MemoryDebugEntry[]> {
|
||||
const config = normalizeMiddlewareConfig(middlewareConfig)
|
||||
const query = resolveProfileQuery(lastUserMessage, memoryMode)
|
||||
|
||||
try {
|
||||
const apiKey =
|
||||
supermemoryApiKey?.trim() || process.env.SUPERMEMORY_API_KEY?.trim()
|
||||
if (!apiKey) throw new Error("Supermemory API key is required")
|
||||
const profile = await fetchProfileContext(
|
||||
getSupermemoryClient(apiKey),
|
||||
containerTag,
|
||||
query,
|
||||
signal,
|
||||
)
|
||||
const selectedProfile = selectProfileForMode(profile, memoryMode)
|
||||
const summary = summarizeProfile(selectedProfile)
|
||||
|
||||
return [
|
||||
{
|
||||
type: "context_reconstruction",
|
||||
label: "Post-response context reconstruction",
|
||||
detail: {
|
||||
authoritativeMiddlewareCapture: false,
|
||||
timing: "after model response",
|
||||
endpoint: "POST /v4/profile",
|
||||
containerTag,
|
||||
customId: conversationId,
|
||||
memoryMode,
|
||||
addMemory: config.addMemory,
|
||||
verbose: config.verbose,
|
||||
...(aiSdkExtras?.includeToolCalls !== undefined
|
||||
? { includeToolCalls: aiSdkExtras.includeToolCalls }
|
||||
: {}),
|
||||
...(aiSdkExtras?.skipMemoryOnError !== undefined
|
||||
? { skipMemoryOnError: aiSdkExtras.skipMemoryOnError }
|
||||
: {}),
|
||||
query: query ?? null,
|
||||
...summary,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "context_preview",
|
||||
label: "Reconstructed context preview (not middleware capture)",
|
||||
preview: buildContextPreview(selectedProfile, memoryMode, query),
|
||||
},
|
||||
config.addMemory === "always"
|
||||
? {
|
||||
type: "conversation_save_requested",
|
||||
label: "Conversation save requested by middleware",
|
||||
detail: {
|
||||
confirmed: false,
|
||||
containerTag,
|
||||
customId: conversationId,
|
||||
addMemory: config.addMemory,
|
||||
verbose: config.verbose,
|
||||
...(aiSdkExtras?.includeToolCalls !== undefined
|
||||
? { includeToolCalls: aiSdkExtras.includeToolCalls }
|
||||
: {}),
|
||||
},
|
||||
}
|
||||
: {
|
||||
type: "conversation_save_skipped",
|
||||
label: "Conversation saving disabled",
|
||||
detail: { addMemory: config.addMemory },
|
||||
},
|
||||
]
|
||||
} catch (error) {
|
||||
return [
|
||||
{
|
||||
type: "debug_error",
|
||||
label: "Post-response context reconstruction unavailable",
|
||||
detail: {
|
||||
nonFatal: true,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
30
apps/sdk-playground/src/lib/middleware-config.ts
Normal file
30
apps/sdk-playground/src/lib/middleware-config.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
export type AddMemoryMode = "always" | "never"
|
||||
export type MemoryMode = "profile" | "query" | "full"
|
||||
|
||||
export interface MiddlewareRuntimeConfig {
|
||||
addMemory: AddMemoryMode
|
||||
verbose: boolean
|
||||
includeToolCalls: boolean
|
||||
skipMemoryOnError: boolean
|
||||
}
|
||||
|
||||
export const DEFAULT_MIDDLEWARE_CONFIG: MiddlewareRuntimeConfig = {
|
||||
addMemory: "always",
|
||||
verbose: false,
|
||||
includeToolCalls: false,
|
||||
skipMemoryOnError: true,
|
||||
}
|
||||
|
||||
export function normalizeMiddlewareConfig(
|
||||
input?: Partial<MiddlewareRuntimeConfig> | null,
|
||||
): MiddlewareRuntimeConfig {
|
||||
if (!input) return { ...DEFAULT_MIDDLEWARE_CONFIG }
|
||||
return {
|
||||
addMemory: input.addMemory ?? DEFAULT_MIDDLEWARE_CONFIG.addMemory,
|
||||
verbose: input.verbose ?? DEFAULT_MIDDLEWARE_CONFIG.verbose,
|
||||
includeToolCalls:
|
||||
input.includeToolCalls ?? DEFAULT_MIDDLEWARE_CONFIG.includeToolCalls,
|
||||
skipMemoryOnError:
|
||||
input.skipMemoryOnError ?? DEFAULT_MIDDLEWARE_CONFIG.skipMemoryOnError,
|
||||
}
|
||||
}
|
||||
354
apps/sdk-playground/src/lib/request-validation.ts
Normal file
354
apps/sdk-playground/src/lib/request-validation.ts
Normal file
|
|
@ -0,0 +1,354 @@
|
|||
import type { PlaygroundApiKeys } from "./api-keys"
|
||||
import type { ChatMessage } from "./chat-handlers"
|
||||
import type { MiddlewareRuntimeConfig } from "./middleware-config"
|
||||
|
||||
const MAX_BODY_BYTES = 256_000
|
||||
const MAX_MESSAGES = 64
|
||||
const MAX_MESSAGE_LENGTH = 20_000
|
||||
const MAX_TOTAL_MESSAGE_LENGTH = 100_000
|
||||
const MAX_IDENTIFIER_LENGTH = 256
|
||||
const MAX_API_KEY_LENGTH = 1_024
|
||||
const MAX_CONTAINER_TAG_LENGTH = 100
|
||||
const MAX_CONVERSATION_ID_LENGTH = 242
|
||||
const CONTAINER_TAG_PATTERN = /^[a-zA-Z0-9_:-]+$/
|
||||
|
||||
export class PlaygroundRequestError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
readonly status: number,
|
||||
) {
|
||||
super(message)
|
||||
this.name = "PlaygroundRequestError"
|
||||
}
|
||||
}
|
||||
|
||||
function firstHeaderValue(value: string | null): string | null {
|
||||
const first = value?.split(",", 1)[0]?.trim()
|
||||
return first || null
|
||||
}
|
||||
|
||||
function requestOrigin(request: Request): string | null {
|
||||
const internalUrl = new URL(request.url)
|
||||
const forwardedHostHeader = request.headers.get("x-forwarded-host")
|
||||
const forwardedProtoHeader = request.headers.get("x-forwarded-proto")
|
||||
const hostHeader = request.headers.get("host")
|
||||
const directHost = firstHeaderValue(hostHeader)
|
||||
if (hostHeader !== null && !directHost) return null
|
||||
const forwardedHost = firstHeaderValue(forwardedHostHeader)
|
||||
if (forwardedHostHeader !== null && !forwardedHost) return null
|
||||
// Portless preserves the routed Host header but may preserve a client-supplied
|
||||
// X-Forwarded-Host. Trust Host first so XFH cannot widen env-key access.
|
||||
const host = directHost ?? forwardedHost
|
||||
if (!host) return internalUrl.origin
|
||||
|
||||
const forwardedProto = firstHeaderValue(forwardedProtoHeader)
|
||||
if (
|
||||
forwardedProtoHeader !== null &&
|
||||
forwardedProto !== "http" &&
|
||||
forwardedProto !== "https"
|
||||
) {
|
||||
return null
|
||||
}
|
||||
const protocol =
|
||||
forwardedProto === "http" || forwardedProto === "https"
|
||||
? forwardedProto
|
||||
: internalUrl.protocol.slice(0, -1)
|
||||
|
||||
try {
|
||||
const externalUrl = new URL(`${protocol}://${host}`)
|
||||
if (
|
||||
externalUrl.host.toLowerCase() !== host.toLowerCase() ||
|
||||
externalUrl.username ||
|
||||
externalUrl.password ||
|
||||
externalUrl.pathname !== "/" ||
|
||||
externalUrl.search ||
|
||||
externalUrl.hash
|
||||
) {
|
||||
return null
|
||||
}
|
||||
return externalUrl.origin
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function assertTrustedBrowserRequest(request: Request): void {
|
||||
if (request.headers.get("sec-fetch-site") === "cross-site") {
|
||||
throw new PlaygroundRequestError("Cross-site requests are not allowed", 403)
|
||||
}
|
||||
|
||||
const expectedOrigin = requestOrigin(request)
|
||||
if (!expectedOrigin) {
|
||||
throw new PlaygroundRequestError("Request host is not allowed", 403)
|
||||
}
|
||||
|
||||
const origin = request.headers.get("origin")
|
||||
let normalizedOrigin: string | null = null
|
||||
if (origin) {
|
||||
try {
|
||||
normalizedOrigin = new URL(origin).origin
|
||||
} catch {
|
||||
throw new PlaygroundRequestError("Request origin is not allowed", 403)
|
||||
}
|
||||
}
|
||||
if (normalizedOrigin && normalizedOrigin !== expectedOrigin) {
|
||||
throw new PlaygroundRequestError("Request origin is not allowed", 403)
|
||||
}
|
||||
}
|
||||
|
||||
export function mayUseEnvironmentKeys(request: Request): boolean {
|
||||
if (process.env.SDK_PLAYGROUND_ALLOW_ENV_KEYS === "true") return true
|
||||
|
||||
const origin = requestOrigin(request)
|
||||
if (!origin) return false
|
||||
const hostname = new URL(origin).hostname.toLowerCase()
|
||||
return (
|
||||
hostname === "localhost" ||
|
||||
hostname === "127.0.0.1" ||
|
||||
hostname === "[::1]" ||
|
||||
hostname.endsWith(".localhost") ||
|
||||
hostname === "sdk.dev.supermemory.ai"
|
||||
)
|
||||
}
|
||||
|
||||
export async function readJsonObject(
|
||||
request: Request,
|
||||
): Promise<Record<string, unknown>> {
|
||||
const contentType = request.headers.get("content-type") ?? ""
|
||||
if (!contentType.toLowerCase().startsWith("application/json")) {
|
||||
throw new PlaygroundRequestError(
|
||||
"Content-Type must be application/json",
|
||||
415,
|
||||
)
|
||||
}
|
||||
|
||||
const contentLength = Number(request.headers.get("content-length"))
|
||||
if (Number.isFinite(contentLength) && contentLength > MAX_BODY_BYTES) {
|
||||
throw new PlaygroundRequestError("Request body is too large", 413)
|
||||
}
|
||||
|
||||
const raw = await request.text()
|
||||
if (new TextEncoder().encode(raw).byteLength > MAX_BODY_BYTES) {
|
||||
throw new PlaygroundRequestError("Request body is too large", 413)
|
||||
}
|
||||
|
||||
let value: unknown
|
||||
try {
|
||||
value = JSON.parse(raw)
|
||||
} catch {
|
||||
throw new PlaygroundRequestError("Request body must be valid JSON", 400)
|
||||
}
|
||||
|
||||
if (!isRecord(value)) {
|
||||
throw new PlaygroundRequestError("Request body must be a JSON object", 400)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
export function parseMessages(value: unknown): ChatMessage[] {
|
||||
if (!Array.isArray(value) || value.length === 0) {
|
||||
throw new PlaygroundRequestError(
|
||||
"At least one chat message is required",
|
||||
400,
|
||||
)
|
||||
}
|
||||
if (value.length > MAX_MESSAGES) {
|
||||
throw new PlaygroundRequestError(
|
||||
`A maximum of ${MAX_MESSAGES} messages is allowed`,
|
||||
400,
|
||||
)
|
||||
}
|
||||
|
||||
let totalLength = 0
|
||||
const messages = value.map((item, index): ChatMessage => {
|
||||
if (!isRecord(item)) {
|
||||
throw new PlaygroundRequestError(`Message ${index + 1} is invalid`, 400)
|
||||
}
|
||||
if (
|
||||
item.role !== "user" &&
|
||||
item.role !== "assistant" &&
|
||||
item.role !== "system"
|
||||
) {
|
||||
throw new PlaygroundRequestError(
|
||||
`Message ${index + 1} has an invalid role`,
|
||||
400,
|
||||
)
|
||||
}
|
||||
if (typeof item.content !== "string") {
|
||||
throw new PlaygroundRequestError(
|
||||
`Message ${index + 1} content must be text`,
|
||||
400,
|
||||
)
|
||||
}
|
||||
if (item.content.length > MAX_MESSAGE_LENGTH) {
|
||||
throw new PlaygroundRequestError(
|
||||
`Message ${index + 1} exceeds ${MAX_MESSAGE_LENGTH} characters`,
|
||||
400,
|
||||
)
|
||||
}
|
||||
totalLength += item.content.length
|
||||
return { role: item.role, content: item.content }
|
||||
})
|
||||
|
||||
if (totalLength > MAX_TOTAL_MESSAGE_LENGTH) {
|
||||
throw new PlaygroundRequestError("Chat history is too large", 400)
|
||||
}
|
||||
if (
|
||||
!messages.some(
|
||||
(message) => message.role === "user" && message.content.trim().length > 0,
|
||||
)
|
||||
) {
|
||||
throw new PlaygroundRequestError(
|
||||
"Chat history must include a non-empty user message",
|
||||
400,
|
||||
)
|
||||
}
|
||||
return messages
|
||||
}
|
||||
|
||||
export function parseIdentifier(
|
||||
value: unknown,
|
||||
name: string,
|
||||
fallback?: string,
|
||||
): string {
|
||||
const resolved = typeof value === "string" ? value.trim() : fallback
|
||||
if (!resolved) {
|
||||
throw new PlaygroundRequestError(`${name} is required`, 400)
|
||||
}
|
||||
if (resolved.length > MAX_IDENTIFIER_LENGTH) {
|
||||
throw new PlaygroundRequestError(
|
||||
`${name} must be ${MAX_IDENTIFIER_LENGTH} characters or fewer`,
|
||||
400,
|
||||
)
|
||||
}
|
||||
return resolved
|
||||
}
|
||||
|
||||
export function parseContainerTag(
|
||||
value: unknown,
|
||||
fallback = "sdk-playground",
|
||||
): string {
|
||||
const containerTag = parseIdentifier(value, "containerTag", fallback)
|
||||
if (containerTag.length > MAX_CONTAINER_TAG_LENGTH) {
|
||||
throw new PlaygroundRequestError(
|
||||
`containerTag must be ${MAX_CONTAINER_TAG_LENGTH} characters or fewer`,
|
||||
400,
|
||||
)
|
||||
}
|
||||
if (!CONTAINER_TAG_PATTERN.test(containerTag)) {
|
||||
throw new PlaygroundRequestError(
|
||||
"containerTag may only contain letters, numbers, hyphens, underscores, and colons",
|
||||
400,
|
||||
)
|
||||
}
|
||||
return containerTag
|
||||
}
|
||||
|
||||
export function parseConversationId(value: unknown): string {
|
||||
const conversationId = parseIdentifier(value, "conversationId")
|
||||
if (conversationId.length > MAX_CONVERSATION_ID_LENGTH) {
|
||||
throw new PlaygroundRequestError(
|
||||
`conversationId must be ${MAX_CONVERSATION_ID_LENGTH} characters or fewer`,
|
||||
400,
|
||||
)
|
||||
}
|
||||
return conversationId
|
||||
}
|
||||
|
||||
export function parseOptionalText(
|
||||
value: unknown,
|
||||
name: string,
|
||||
maxLength = MAX_MESSAGE_LENGTH,
|
||||
): string | undefined {
|
||||
if (value === undefined || value === null || value === "") return undefined
|
||||
if (typeof value !== "string") {
|
||||
throw new PlaygroundRequestError(`${name} must be text`, 400)
|
||||
}
|
||||
const resolved = value.trim()
|
||||
if (!resolved) return undefined
|
||||
if (resolved.length > maxLength) {
|
||||
throw new PlaygroundRequestError(
|
||||
`${name} must be ${maxLength} characters or fewer`,
|
||||
400,
|
||||
)
|
||||
}
|
||||
return resolved
|
||||
}
|
||||
|
||||
export function parseMemoryMode(
|
||||
value: unknown,
|
||||
): "profile" | "query" | "full" | undefined {
|
||||
if (value === undefined || value === null) return undefined
|
||||
if (value === "profile" || value === "query" || value === "full") {
|
||||
return value
|
||||
}
|
||||
throw new PlaygroundRequestError("Invalid memory mode", 400)
|
||||
}
|
||||
|
||||
export function parseMiddlewareConfig(
|
||||
value: unknown,
|
||||
): Partial<MiddlewareRuntimeConfig> | undefined {
|
||||
if (value === undefined || value === null) return undefined
|
||||
if (!isRecord(value)) {
|
||||
throw new PlaygroundRequestError("Invalid middleware configuration", 400)
|
||||
}
|
||||
|
||||
if (
|
||||
value.addMemory !== undefined &&
|
||||
value.addMemory !== "always" &&
|
||||
value.addMemory !== "never"
|
||||
) {
|
||||
throw new PlaygroundRequestError("Invalid addMemory value", 400)
|
||||
}
|
||||
for (const key of [
|
||||
"verbose",
|
||||
"includeToolCalls",
|
||||
"skipMemoryOnError",
|
||||
] as const) {
|
||||
if (value[key] !== undefined && typeof value[key] !== "boolean") {
|
||||
throw new PlaygroundRequestError(`Invalid ${key} value`, 400)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...(value.addMemory !== undefined
|
||||
? { addMemory: value.addMemory as "always" | "never" }
|
||||
: {}),
|
||||
...(value.verbose !== undefined
|
||||
? { verbose: value.verbose as boolean }
|
||||
: {}),
|
||||
...(value.includeToolCalls !== undefined
|
||||
? { includeToolCalls: value.includeToolCalls as boolean }
|
||||
: {}),
|
||||
...(value.skipMemoryOnError !== undefined
|
||||
? { skipMemoryOnError: value.skipMemoryOnError as boolean }
|
||||
: {}),
|
||||
}
|
||||
}
|
||||
|
||||
export function parseApiKeys(value: unknown): Partial<PlaygroundApiKeys> {
|
||||
if (value === undefined || value === null) return {}
|
||||
if (!isRecord(value)) {
|
||||
throw new PlaygroundRequestError("Invalid API key configuration", 400)
|
||||
}
|
||||
|
||||
return {
|
||||
supermemoryApiKey: parseOptionalApiKey(
|
||||
value.supermemoryApiKey,
|
||||
"Supermemory API key",
|
||||
),
|
||||
openaiApiKey: parseOptionalApiKey(value.openaiApiKey, "OpenAI API key"),
|
||||
}
|
||||
}
|
||||
|
||||
function parseOptionalApiKey(value: unknown, name: string): string {
|
||||
if (value === undefined || value === null || value === "") return ""
|
||||
if (typeof value !== "string" || value.length > MAX_API_KEY_LENGTH) {
|
||||
throw new PlaygroundRequestError(`${name} is invalid`, 400)
|
||||
}
|
||||
return value.trim()
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
}
|
||||
116
apps/sdk-playground/src/lib/sdk-registry.ts
Normal file
116
apps/sdk-playground/src/lib/sdk-registry.ts
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
export type SdkLanguage = "typescript" | "python"
|
||||
export type IntegrationMode = "middleware" | "tools" | "direct"
|
||||
|
||||
export interface ToolTraceEntry {
|
||||
step: number
|
||||
toolName: string
|
||||
args: unknown
|
||||
result?: unknown
|
||||
}
|
||||
|
||||
export interface ChatSdkDefinition {
|
||||
id: string
|
||||
label: string
|
||||
language: SdkLanguage
|
||||
mode: IntegrationMode
|
||||
package: string
|
||||
description: string
|
||||
available: boolean
|
||||
}
|
||||
|
||||
export const CHAT_SDK_REGISTRY: ChatSdkDefinition[] = [
|
||||
{
|
||||
id: "ts-ai-sdk-middleware",
|
||||
label: "AI SDK + middleware",
|
||||
language: "typescript",
|
||||
mode: "middleware",
|
||||
package: "@supermemory/tools/ai-sdk",
|
||||
description:
|
||||
"withSupermemory wraps the model — auto-injects profile/search and saves conversations",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: "ts-openai-middleware",
|
||||
label: "OpenAI SDK + middleware",
|
||||
language: "typescript",
|
||||
mode: "middleware",
|
||||
package: "@supermemory/tools/openai",
|
||||
description:
|
||||
"withSupermemory on OpenAI client — same automatic memory path",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: "ts-ai-sdk-tools",
|
||||
label: "AI SDK + tools",
|
||||
language: "typescript",
|
||||
mode: "tools",
|
||||
package: "@supermemory/tools/ai-sdk",
|
||||
description:
|
||||
"Agent explicitly calls the 7 Supermemory tools via generateText",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: "ts-openai-tools",
|
||||
label: "OpenAI SDK + tools",
|
||||
language: "typescript",
|
||||
mode: "tools",
|
||||
package: "@supermemory/tools/openai",
|
||||
description: "OpenAI function calling with the 7 Supermemory tools",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: "ts-ai-sdk-package",
|
||||
label: "@supermemory/ai-sdk",
|
||||
language: "typescript",
|
||||
mode: "tools",
|
||||
package: "@supermemory/ai-sdk",
|
||||
description: "Re-export of tools/ai-sdk — same 7-tool agent",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: "py-openai-middleware",
|
||||
label: "OpenAI + middleware",
|
||||
language: "python",
|
||||
mode: "middleware",
|
||||
package: "supermemory-openai-sdk",
|
||||
description:
|
||||
"with_supermemory — automatic profile injection + conversation save",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: "py-openai-tools",
|
||||
label: "OpenAI + tools",
|
||||
language: "python",
|
||||
mode: "tools",
|
||||
package: "supermemory-openai-sdk",
|
||||
description: "SupermemoryTools function-calling loop (7 tools)",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: "py-supermemory-direct",
|
||||
label: "supermemory + manual context",
|
||||
language: "python",
|
||||
mode: "direct",
|
||||
package: "supermemory",
|
||||
description: "profile() then OpenAI — manual integration pattern from docs",
|
||||
available: true,
|
||||
},
|
||||
]
|
||||
|
||||
export const PYTHON_SERVER_URL =
|
||||
process.env.SDK_PLAYGROUND_PYTHON_URL ?? "http://127.0.0.1:8792"
|
||||
|
||||
export const TOOLS_SYSTEM_PROMPT = `You are a helpful assistant with Supermemory long-term memory.
|
||||
|
||||
You have tools to manage memory. Use them proactively:
|
||||
- searchMemories: hybrid recall — search before answering whenever user-specific context could help (do not wait to be asked)
|
||||
- getProfile: broad static/dynamic user context at conversation start or when you need a wide overview
|
||||
- addMemory: store a new generalizable fact
|
||||
- documentList / documentAdd / documentDelete: manage source documents (documentDelete is permanent)
|
||||
- memoryForget: soft-delete one profile fact by memoryId or exact content (not whole documents)
|
||||
|
||||
Before answering questions about the user, their preferences, or past context, search memories or get profile first. When the user asks you to remember something, use addMemory.`
|
||||
|
||||
export function getChatSdk(id: string): ChatSdkDefinition | undefined {
|
||||
return CHAT_SDK_REGISTRY.find((s) => s.id === id)
|
||||
}
|
||||
160
apps/sdk-playground/src/lib/tools-catalog.ts
Normal file
160
apps/sdk-playground/src/lib/tools-catalog.ts
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
import {
|
||||
PARAMETER_DESCRIPTIONS,
|
||||
TOOL_DESCRIPTIONS,
|
||||
} from "../../../../packages/tools/src/tools-shared"
|
||||
|
||||
export interface CatalogParameter {
|
||||
name: string
|
||||
/** Omitted when this parameter is only exposed by the TypeScript tool schema. */
|
||||
pythonName?: string
|
||||
description: string
|
||||
required?: boolean
|
||||
}
|
||||
|
||||
export interface CatalogTool {
|
||||
id: string
|
||||
pythonName: string
|
||||
description: string
|
||||
parameters: CatalogParameter[]
|
||||
}
|
||||
|
||||
export const TOOL_CATALOG: CatalogTool[] = [
|
||||
{
|
||||
id: "searchMemories",
|
||||
pythonName: "search_memories",
|
||||
description: TOOL_DESCRIPTIONS.searchMemories,
|
||||
parameters: [
|
||||
{
|
||||
name: "informationToGet",
|
||||
pythonName: "information_to_get",
|
||||
description: PARAMETER_DESCRIPTIONS.informationToGet,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "includeFullDocs",
|
||||
description: PARAMETER_DESCRIPTIONS.includeFullDocs,
|
||||
},
|
||||
{
|
||||
name: "limit",
|
||||
pythonName: "limit",
|
||||
description: PARAMETER_DESCRIPTIONS.limit,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "addMemory",
|
||||
pythonName: "add_memory",
|
||||
description: TOOL_DESCRIPTIONS.addMemory,
|
||||
parameters: [
|
||||
{
|
||||
name: "memory",
|
||||
pythonName: "memory",
|
||||
description: PARAMETER_DESCRIPTIONS.memory,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "getProfile",
|
||||
pythonName: "get_profile",
|
||||
description: TOOL_DESCRIPTIONS.getProfile,
|
||||
parameters: [
|
||||
{
|
||||
name: "containerTag",
|
||||
description: PARAMETER_DESCRIPTIONS.containerTag,
|
||||
},
|
||||
{
|
||||
name: "query",
|
||||
pythonName: "query",
|
||||
description: PARAMETER_DESCRIPTIONS.query,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "documentList",
|
||||
pythonName: "document_list",
|
||||
description: TOOL_DESCRIPTIONS.documentList,
|
||||
parameters: [
|
||||
{
|
||||
name: "containerTag",
|
||||
description: PARAMETER_DESCRIPTIONS.containerTag,
|
||||
},
|
||||
{
|
||||
name: "limit",
|
||||
pythonName: "limit",
|
||||
description: PARAMETER_DESCRIPTIONS.limit,
|
||||
},
|
||||
{
|
||||
name: "page",
|
||||
pythonName: "page",
|
||||
description: PARAMETER_DESCRIPTIONS.page,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "documentDelete",
|
||||
pythonName: "document_delete",
|
||||
description: TOOL_DESCRIPTIONS.documentDelete,
|
||||
parameters: [
|
||||
{
|
||||
name: "documentId",
|
||||
pythonName: "document_id",
|
||||
description: PARAMETER_DESCRIPTIONS.documentId,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "containerTag",
|
||||
description: PARAMETER_DESCRIPTIONS.documentContainerTag,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "documentAdd",
|
||||
pythonName: "document_add",
|
||||
description: TOOL_DESCRIPTIONS.documentAdd,
|
||||
parameters: [
|
||||
{
|
||||
name: "content",
|
||||
pythonName: "content",
|
||||
description: PARAMETER_DESCRIPTIONS.content,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "title",
|
||||
pythonName: "title",
|
||||
description: PARAMETER_DESCRIPTIONS.title,
|
||||
},
|
||||
{
|
||||
name: "description",
|
||||
pythonName: "description",
|
||||
description: PARAMETER_DESCRIPTIONS.description,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "memoryForget",
|
||||
pythonName: "memory_forget",
|
||||
description: TOOL_DESCRIPTIONS.memoryForget,
|
||||
parameters: [
|
||||
{
|
||||
name: "containerTag",
|
||||
description: PARAMETER_DESCRIPTIONS.containerTag,
|
||||
},
|
||||
{
|
||||
name: "memoryId",
|
||||
pythonName: "memory_id",
|
||||
description: PARAMETER_DESCRIPTIONS.memoryId,
|
||||
},
|
||||
{
|
||||
name: "memoryContent",
|
||||
pythonName: "memory_content",
|
||||
description: PARAMETER_DESCRIPTIONS.memoryContent,
|
||||
},
|
||||
{
|
||||
name: "reason",
|
||||
pythonName: "reason",
|
||||
description: PARAMETER_DESCRIPTIONS.reason,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
33
apps/sdk-playground/tsconfig.json
Normal file
33
apps/sdk-playground/tsconfig.json
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
108
bun.lock
108
bun.lock
|
|
@ -142,6 +142,30 @@
|
|||
"typescript": "^5",
|
||||
},
|
||||
},
|
||||
"apps/sdk-playground": {
|
||||
"name": "sdk-playground",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai": "^2.0.22",
|
||||
"@supermemory/ai-sdk": "workspace:*",
|
||||
"@supermemory/tools": "workspace:*",
|
||||
"ai": "^5.0.113",
|
||||
"next": "16.0.7",
|
||||
"openai": "^4.104.0",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"supermemory": "^4.25.4",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"concurrently": "^9.1.2",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5",
|
||||
},
|
||||
},
|
||||
"apps/web": {
|
||||
"name": "@repo/web",
|
||||
"version": "0.1.0",
|
||||
|
|
@ -261,7 +285,7 @@
|
|||
"dependencies": {
|
||||
"@ai-sdk/openai": "^2.0.22",
|
||||
"@ai-sdk/provider": "^2.0.0",
|
||||
"@supermemory/tools": "workspace:*",
|
||||
"@supermemory/tools": "^2.2.0",
|
||||
"ai": "^5.0.113",
|
||||
"supermemory": "^4.25.4",
|
||||
},
|
||||
|
|
@ -2580,6 +2604,8 @@
|
|||
|
||||
"concat-stream": ["concat-stream@1.6.2", "", { "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^2.2.2", "typedarray": "^0.0.6" } }, "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw=="],
|
||||
|
||||
"concurrently": ["concurrently@9.2.4", "", { "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", "shell-quote": "1.9.0", "supports-color": "8.1.1", "tree-kill": "1.2.2", "yargs": "17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", "concurrently": "dist/bin/concurrently.js" } }, "sha512-TZ0CEhyzvFjgtAvHTusDMgj7wNdihCh7LLLrzdUOXIhdlnL2JBBGA9eJxR24rtqgmdjh3OA3hrN1rCHj6HM8qA=="],
|
||||
|
||||
"confbox": ["confbox@0.2.4", "", {}, "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="],
|
||||
|
||||
"config-chain": ["config-chain@1.1.13", "", { "dependencies": { "ini": "^1.3.4", "proto-list": "~1.2.1" } }, "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ=="],
|
||||
|
|
@ -4448,6 +4474,8 @@
|
|||
|
||||
"scule": ["scule@1.3.0", "", {}, "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g=="],
|
||||
|
||||
"sdk-playground": ["sdk-playground@workspace:apps/sdk-playground"],
|
||||
|
||||
"section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="],
|
||||
|
||||
"secure-json-parse": ["secure-json-parse@2.7.0", "", {}, "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="],
|
||||
|
|
@ -4484,7 +4512,7 @@
|
|||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
|
||||
"shell-quote": ["shell-quote@1.7.3", "", {}, "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw=="],
|
||||
"shell-quote": ["shell-quote@1.9.0", "", {}, "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA=="],
|
||||
|
||||
"shellwords": ["shellwords@0.1.1", "", {}, "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww=="],
|
||||
|
||||
|
|
@ -4628,7 +4656,7 @@
|
|||
|
||||
"supermemory-mcp": ["supermemory-mcp@workspace:apps/mcp"],
|
||||
|
||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
"supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
|
||||
|
||||
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||
|
||||
|
|
@ -5654,6 +5682,10 @@
|
|||
|
||||
"compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"concurrently/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
"concurrently/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
|
||||
|
||||
"config-chain/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
|
||||
|
||||
"cosmiconfig/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
|
||||
|
|
@ -5756,6 +5788,8 @@
|
|||
|
||||
"fx-runner/commander": ["commander@2.9.0", "", { "dependencies": { "graceful-readlink": ">= 1.0.0" } }, "sha512-bmkUukX8wAOjHdN26xj5c4ctEV22TQ7dQYhSmuckKhToXrkUn0iIaolHdIxYYqD55nhpSPA9zPQ1yP57GdXP2A=="],
|
||||
|
||||
"fx-runner/shell-quote": ["shell-quote@1.7.3", "", {}, "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw=="],
|
||||
|
||||
"fx-runner/which": ["which@1.2.4", "", { "dependencies": { "is-absolute": "^0.1.7", "isexe": "^1.1.1" }, "bin": { "which": "./bin/which" } }, "sha512-zDRAqDSBudazdfM9zpiI30Fu9ve47htYXcGi3ln0wfKu2a7SmrT6F3VDoYONu//48V8Vz4TdCRNPjtvyRO3yBA=="],
|
||||
|
||||
"gaxios/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
||||
|
|
@ -5794,8 +5828,6 @@
|
|||
|
||||
"is-online/got": ["got@12.6.1", "", { "dependencies": { "@sindresorhus/is": "^5.2.0", "@szmarczak/http-timer": "^5.0.1", "cacheable-lookup": "^7.0.0", "cacheable-request": "^10.2.8", "decompress-response": "^6.0.0", "form-data-encoder": "^2.1.2", "get-stream": "^6.0.1", "http2-wrapper": "^2.1.10", "lowercase-keys": "^3.0.0", "p-cancelable": "^3.0.0", "responselike": "^3.0.0" } }, "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ=="],
|
||||
|
||||
"jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
|
||||
|
||||
"jsonwebtoken/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||
|
||||
"katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
|
||||
|
|
@ -5940,6 +5972,18 @@
|
|||
|
||||
"schema-utils/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
|
||||
|
||||
"sdk-playground/@types/node": ["@types/node@20.19.35", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ=="],
|
||||
|
||||
"sdk-playground/next": ["next@16.0.7", "", { "dependencies": { "@next/env": "16.0.7", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.0.7", "@next/swc-darwin-x64": "16.0.7", "@next/swc-linux-arm64-gnu": "16.0.7", "@next/swc-linux-arm64-musl": "16.0.7", "@next/swc-linux-x64-gnu": "16.0.7", "@next/swc-linux-x64-musl": "16.0.7", "@next/swc-win32-arm64-msvc": "16.0.7", "@next/swc-win32-x64-msvc": "16.0.7", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-3mBRJyPxT4LOxAJI6IsXeFtKfiJUbjCLgvXO02fV8Wy/lIhPvP94Fe7dGhUgHXcQy4sSuYwQNcOLhIfOm0rL0A=="],
|
||||
|
||||
"sdk-playground/react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="],
|
||||
|
||||
"sdk-playground/react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="],
|
||||
|
||||
"sdk-playground/supermemory": ["supermemory@4.25.4", "", { "bin": { "supermemory": "bin/cli" } }, "sha512-97ME3rlmu7OmsXJTb9OgXOD+3VUv4Wej0ZX9xezG+LKkMwrzi4xeeAZaOJFcr0oI/QQjcHG2WOzm+und1e7MFA=="],
|
||||
|
||||
"sdk-playground/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"send/http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||
|
|
@ -6590,6 +6634,8 @@
|
|||
|
||||
"@sap-cloud-sdk/util/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"@sap-cloud-sdk/util/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"@sentry/bundler-plugin-core/glob/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
|
||||
|
||||
"@sentry/bundler-plugin-core/glob/path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="],
|
||||
|
|
@ -6680,6 +6726,16 @@
|
|||
|
||||
"compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"concurrently/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"concurrently/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"concurrently/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
||||
|
||||
"concurrently/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"concurrently/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
||||
|
||||
"cosmiconfig/parse-json/lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
|
||||
|
||||
"cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="],
|
||||
|
|
@ -6706,6 +6762,8 @@
|
|||
|
||||
"eslint/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"eslint/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"eslint/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
|
@ -6754,6 +6812,8 @@
|
|||
|
||||
"find-process/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"find-process/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"front-matter/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
|
||||
|
||||
"fx-runner/which/isexe": ["isexe@1.1.2", "", {}, "sha512-d2eJzK691yZwPHcv1LbeAOa91yMJ9QmfTgSO1oXB65ezVhXQsxBac2vEB4bMVms9cGzaA99n6V2viHMq82VLDw=="],
|
||||
|
|
@ -6824,6 +6884,28 @@
|
|||
|
||||
"raw-body/http-errors/statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||
|
||||
"sdk-playground/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"sdk-playground/next/@next/env": ["@next/env@16.0.7", "", {}, "sha512-gpaNgUh5nftFKRkRQGnVi5dpcYSKGcZZkQffZ172OrG/XkrnS7UBTQ648YY+8ME92cC4IojpI2LqTC8sTDhAaw=="],
|
||||
|
||||
"sdk-playground/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.0.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LlDtCYOEj/rfSnEn/Idi+j1QKHxY9BJFmxx7108A6D8K0SB+bNgfYQATPk/4LqOl4C0Wo3LACg2ie6s7xqMpJg=="],
|
||||
|
||||
"sdk-playground/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.0.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rtZ7BhnVvO1ICf3QzfW9H3aPz7GhBrnSIMZyr4Qy6boXF0b5E3QLs+cvJmg3PsTCG2M1PBoC+DANUi4wCOKXpA=="],
|
||||
|
||||
"sdk-playground/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.0.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-mloD5WcPIeIeeZqAIP5c2kdaTa6StwP4/2EGy1mUw8HiexSHGK/jcM7lFuS3u3i2zn+xH9+wXJs6njO7VrAqww=="],
|
||||
|
||||
"sdk-playground/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.0.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-+ksWNrZrthisXuo9gd1XnjHRowCbMtl/YgMpbRvFeDEqEBd523YHPWpBuDjomod88U8Xliw5DHhekBC3EOOd9g=="],
|
||||
|
||||
"sdk-playground/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.0.7", "", { "os": "linux", "cpu": "x64" }, "sha512-4WtJU5cRDxpEE44Ana2Xro1284hnyVpBb62lIpU5k85D8xXxatT+rXxBgPkc7C1XwkZMWpK5rXLXTh9PFipWsA=="],
|
||||
|
||||
"sdk-playground/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.0.7", "", { "os": "linux", "cpu": "x64" }, "sha512-HYlhqIP6kBPXalW2dbMTSuB4+8fe+j9juyxwfMwCe9kQPPeiyFn7NMjNfoFOfJ2eXkeQsoUGXg+O2SE3m4Qg2w=="],
|
||||
|
||||
"sdk-playground/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.0.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-EviG+43iOoBRZg9deGauXExjRphhuYmIOJ12b9sAPy0eQ6iwcPxfED2asb/s2/yiLYOdm37kPaiZu8uXSYPs0Q=="],
|
||||
|
||||
"sdk-playground/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.0.7", "", { "os": "win32", "cpu": "x64" }, "sha512-gniPjy55zp5Eg0896qSrf3yB1dw4F/3s8VK1ephdsZZ129j2n6e1WqCbE2YgcKhW9hPB9TVZENugquWJD5x0ug=="],
|
||||
|
||||
"sdk-playground/next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
||||
|
||||
"send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
|
@ -7336,6 +7418,14 @@
|
|||
|
||||
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"concurrently/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"concurrently/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
|
||||
"concurrently/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"concurrently/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"docs-test/@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"docs-test/@ai-sdk/openai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
|
@ -7348,6 +7438,8 @@
|
|||
|
||||
"memory-graph-playground/next/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"sdk-playground/next/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"supermemory-mcp/agents/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
|
||||
|
||||
"supermemory-mcp/agents/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="],
|
||||
|
|
@ -7502,6 +7594,12 @@
|
|||
|
||||
"agents/@modelcontextprotocol/sdk/express/type-is/media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
|
||||
|
||||
"concurrently/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"concurrently/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"concurrently/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"@mastra/core/@modelcontextprotocol/sdk/express/body-parser/http-errors/statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||
|
||||
"@mintlify/link-rot/@mintlify/scraping/@mintlify/common/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
|
|
|||
|
|
@ -10,6 +10,11 @@
|
|||
"apps/memory-graph-playground": {
|
||||
"name": "graph.dev.supermemory",
|
||||
"script": "dev:app"
|
||||
},
|
||||
"apps/sdk-playground": {
|
||||
"name": "sdk.dev.supermemory",
|
||||
"script": "dev:app",
|
||||
"appPort": 3005
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"inputs": ["$TURBO_DEFAULT$", ".env*"],
|
||||
"outputs": [".next/**", "!.next/cache/**"]
|
||||
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
|
||||
},
|
||||
"lint": {
|
||||
"dependsOn": ["^lint"]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue