supermemory/apps/sdk-playground/python/server.py
MaheshtheDev 5c24e67d60 feat(tools)!: @supermemory/tools 3.0 on the v5 API, retire @supermemory/ai-sdk (#1773)
Every integration (AI SDK, OpenAI, Mastra, VoltAgent, Claude memory) now calls v5 through `supermemory@5`. The config takes one `namespace` instead of `containerTags` or `projectId`, and `withSupermemory` takes `namespace` and `id` instead of `containerTag` and `customId`. No aliases. With no config the tools still use `sm_project_default`.

Conversations are stored as one document per conversation keyed by `id` instead of `/v4/conversations`. `memoryForget` drops `reason`; forgetting by text previews with `forgetMatching` and then forgets exact matches by id. Claude memory marks its files with `metadata.source` instead of a second tag. Search keeps the 2.x defaults so results do not shift.

`packages/ai-sdk` is removed: npm already deprecates it in favor of `@supermemory/tools/ai-sdk`, so its CI steps and playground wiring go too. `apps/sdk-playground` moves to the new tools API and v5 routes, including its Python server.

Tested against production: add, search, profile, list and delete. 112 unit tests pass; type errors drop from 148 to 141, none new. The `supermemory` dependency pins the rc until 5.0.0 is published.
2026-10-06 17:03:47 +00:00

879 lines
29 KiB
Python

"""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 quote, 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
DOCUMENT_LIST_LIMIT = 25
# Matches the v4 profile search defaults the TS middlewares keep (memories mode, 0.6).
PROFILE_SEARCH_THRESHOLD = 0.6
MAX_MESSAGE_LENGTH = 20_000
MAX_MESSAGES = 64
MAX_TOTAL_MESSAGE_LENGTH = 100_000
MAX_API_KEY_LENGTH = 1_024
MAX_NAMESPACE_LENGTH = 100
MAX_CONVERSATION_ID_LENGTH = 242
NAMESPACE_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)
namespace: str = Field(
default="sdk-playground",
min_length=1,
max_length=MAX_NAMESPACE_LENGTH,
pattern=NAMESPACE_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):
namespace: str = Field(
default="sdk-playground",
min_length=1,
max_length=MAX_NAMESPACE_LENGTH,
pattern=NAMESPACE_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],
namespace: 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=namespace,
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],
namespace: 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": [namespace],
}
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 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_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)
def supermemory_http(sm_key: str, timeout: float = HTTP_TIMEOUT_SECONDS):
import httpx
return httpx.AsyncClient(
base_url=supermemory_base_url(),
headers={"Authorization": f"Bearer {sm_key}"},
timeout=timeout,
follow_redirects=False,
)
def namespace_path(namespace: str, suffix: str) -> str:
return f"/ns/{quote(namespace, safe='')}{suffix}"
async def fetch_profile_context(
namespace: str,
sm_key: str,
query: Optional[str] = None,
) -> dict[str, list[Any]]:
async with supermemory_http(sm_key) as http:
profile_request = http.post(namespace_path(namespace, "/profile"), json={})
search_request = (
http.post(
namespace_path(namespace, "/search"),
json={
"query": query,
"searchMode": "memories",
"threshold": PROFILE_SEARCH_THRESHOLD,
},
)
if query
else None
)
responses = await asyncio.gather(
profile_request, *([search_request] if search_request else [])
)
for response in responses:
response.raise_for_status()
profile = responses[0].json().get("profile") or {}
search = responses[1].json() if len(responses) > 1 else {}
return {
"static": profile.get("static") or [],
"dynamic": profile.get("dynamic") or [],
"searchResults": search.get("results") or [],
}
async def chat_supermemory_direct(
messages: list[ChatMessage],
namespace: str,
conversation_id: str,
sm_key: str,
oai_key: str,
) -> tuple[str, str, dict[str, list[Any]]]:
"""Manual pattern: profile + search for context, then OpenAI, then add the conversation."""
from openai import AsyncOpenAI
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_context = await fetch_profile_context(namespace, sm_key, last_user)
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 ""
document_id = direct_conversation_id(conversation_id)
return assistant_text, document_id, profile_context
async def save_direct_conversation(
messages: list[ChatMessage],
assistant_text: str,
namespace: str,
document_id: str,
sm_key: str,
) -> dict[str, Any]:
try:
async with asyncio.timeout(DIRECT_SAVE_TIMEOUT_SECONDS):
async with supermemory_http(sm_key, DIRECT_SAVE_TIMEOUT_SECONDS) as http:
response = await http.post(
namespace_path(namespace, "/document"),
json={
"content": conversation_transcript(messages, assistant_text),
"id": document_id,
"dreaming": "instant",
},
)
response.raise_for_status()
accepted = response.json()
return {
"type": "conversation_save_accepted",
"label": "Full conversation accepted for processing",
"detail": {
"nonFatal": True,
"namespace": namespace,
"id": document_id,
"documentId": accepted.get("id"),
"status": accepted.get("status"),
},
}
except Exception as error:
return {
"type": "conversation_save_failed",
"label": "Conversation save unavailable",
"detail": {
"nonFatal": True,
"namespace": namespace,
"id": document_id,
"error": public_error(error, sm_key),
},
}
async def fetch_document_memories(http: Any, namespace: str, document_id: str) -> list[Any]:
try:
response = await http.get(
namespace_path(namespace, f"/document/{quote(document_id, safe='')}"),
params={"include": "memories"},
)
response.raise_for_status()
return response.json().get("memories") or []
except Exception:
return []
async def fetch_namespace_context(
namespace: 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(namespace, sm_key, query)
async with supermemory_http(sm_key) as http:
docs_response = await http.post(
namespace_path(namespace, "/list/documents"),
params={
"limit": DOCUMENT_LIST_LIMIT,
"sort": "createdAt",
"order": "desc",
},
json={},
)
docs_response.raise_for_status()
docs = docs_response.json()
raw_documents = docs.get("documents") or []
# v5 lists omit memories, so each document is fetched with include=memories.
memories = await asyncio.gather(
*(fetch_document_memories(http, namespace, doc["id"]) for doc in raw_documents)
)
documents = []
for doc, memory_entries in zip(raw_documents, memories):
system = doc.get("system") or {}
documents.append(
{
"id": doc.get("id"),
"title": doc.get("title"),
"status": system.get("status"),
"createdAt": system.get("createdAt"),
"updatedAt": system.get("updatedAt"),
"summary": doc.get("summary"),
"memoryEntries": memory_entries,
}
)
return {
"namespace": namespace,
"query": query,
"profile": profile_context,
"documents": documents,
"pagination": docs.get("pagination"),
}
def reconstruct_python_sdk_memory_block(
memory_mode: str,
profile: dict[str, Any],
) -> tuple[dict[str, list[str]], str]:
from supermemory_openai import convert_profile_to_markdown, deduplicate_memories
from supermemory_openai.utils import wrap_memory_context
deduplicated = deduplicate_memories(
static=profile.get("static", []) if memory_mode != "query" else [],
dynamic=profile.get("dynamic", []) if memory_mode != "query" else [],
search_results=profile.get("searchResults", []),
)
visible_profile = {
"static": deduplicated.static,
"dynamic": deduplicated.dynamic,
"searchResults": (
[] if memory_mode == "profile" else deduplicated.search_results
),
}
profile_data = ""
if memory_mode != "query":
profile_data = convert_profile_to_markdown(
{
"profile": {
"static": visible_profile["static"],
"dynamic": visible_profile["dynamic"],
},
"searchResults": {"results": []},
}
)
search_results_memories = ""
if memory_mode != "profile" and visible_profile["searchResults"]:
search_results_memories = (
"Search results for user's recent message: \n"
+ "\n".join(f"- {memory}" for memory in visible_profile["searchResults"])
)
memories = f"{profile_data}\n{search_results_memories}".strip()
return visible_profile, wrap_memory_context(memories)
def build_middleware_memory_debug(
namespace: 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:
raw_profile = context["profile"]
profile, memory_block = reconstruct_python_sdk_memory_block(
memory_mode,
raw_profile,
)
debug.extend(
(
{
"type": "profile_fetch",
"label": "Post-response context reconstruction",
"detail": {
"authoritativeMiddlewareCapture": False,
"timing": "after model response",
"endpoint": (
"POST /ns/{namespace}/profile + POST /ns/{namespace}/search"
),
"namespace": namespace,
"id": 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": (
"Reconstructed SDK-owned memory block "
"(not middleware capture)"
),
"preview": memory_block,
"detail": {
"totalFacts": (
len(profile.get("static", []))
+ len(profile.get("dynamic", []))
+ len(profile.get("searchResults", []))
),
"fullLength": len(memory_block),
},
},
)
)
save_detail = {
"namespace": namespace,
"id": 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(
namespace: 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(namespace, sm_key, query)
return (
{
"namespace": namespace,
"query": query,
"profile": profile,
},
None,
)
except Exception as error:
return None, public_error(error, sm_key)
@app.get("/context")
async def context_get(
namespace: Annotated[
str,
Query(
min_length=1,
max_length=MAX_NAMESPACE_LENGTH,
pattern=NAMESPACE_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_namespace_context(namespace, 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_namespace_context(req.namespace, 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.namespace,
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.namespace, sm_key, oai_key
)
elif req.sdkId == "py-supermemory-direct":
text, document_id, profile_context = await chat_supermemory_direct(
req.messages,
req.namespace,
req.conversationId,
sm_key,
oai_key,
)
last_user = next(
(m.content for m in reversed(req.messages) if m.role == "user"),
"",
)
direct_debug = (document_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.namespace,
query,
sm_key,
)
memory_debug = build_middleware_memory_debug(
req.namespace,
req.conversationId,
req.memoryMode or "full",
last_user,
ctx,
context_error,
middleware_config,
)
elif direct_debug is not None:
document_id, profile_context, last_user = direct_debug
save_debug = await save_direct_conversation(
req.messages,
text,
req.namespace,
document_id,
sm_key,
)
memory_debug = [
{
"type": "manual_profile",
"label": "Profile context used for this response",
"detail": {
"namespace": req.namespace,
"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")