Skip to content

Commit 5810aad

Browse files
committed
feat: add debug logger
1 parent ef6c299 commit 5810aad

File tree

10 files changed

+293
-82
lines changed

10 files changed

+293
-82
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
data/
12
# Byte-compiled / optimized / DLL files
23
__pycache__/
34
*.py[cod]

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ classifiers = [
1818
]
1919
keywords = ["ai", "memory", "conversation", "llm", "chatbot", "agent"]
2020
dependencies = [
21+
"httpx>=0.28.1",
22+
"numpy>=2.3.4",
23+
"openai>=2.8.0",
24+
"pydantic>=2.12.4",
2125
]
2226

2327
[build-system]

src/memu/app/service.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22

33
import asyncio
44
import json
5-
import os
65
import re
76
from collections.abc import Sequence
87
from typing import Any, cast
98

9+
from pydantic import BaseModel
10+
1011
from memu.app.settings import AppSettings
1112
from memu.llm.http_client import HTTPLLMClient
1213
from memu.memory.repo import InMemoryStore
@@ -27,9 +28,9 @@ def __init__(self, settings: AppSettings):
2728
self.store = InMemoryStore()
2829
backend = (settings.llm_client_backend or "httpx").lower()
2930
self.openai: Any
30-
client_kwargs = {
31+
client_kwargs: dict[str, Any] = {
3132
"base_url": settings.openai_base,
32-
"api_key": os.getenv(settings.openai_api_key_env, ""),
33+
"api_key": settings.openai_api_key,
3334
"chat_model": settings.chat_model,
3435
"embed_model": settings.embed_model,
3536
}
@@ -39,9 +40,9 @@ def __init__(self, settings: AppSettings):
3940
self.openai = OpenAISDKClient(**client_kwargs)
4041
elif backend == "httpx":
4142
self.openai = HTTPLLMClient(
42-
**client_kwargs,
4343
provider=self.settings.llm_http_provider,
4444
endpoint_overrides=self.settings.llm_http_endpoints,
45+
**client_kwargs,
4546
)
4647
else:
4748
msg = f"Unknown llm_client_backend '{settings.llm_client_backend}'"
@@ -89,9 +90,9 @@ async def memorize(self, *, resource_url: str, modality: str, summary_prompt: st
8990
await self._update_category_summaries(category_memory_updates)
9091

9192
return {
92-
"resource": res.model_dump(),
93-
"items": [item.model_dump() for item in items],
94-
"categories": [self.store.categories[c].model_dump() for c in cat_ids],
93+
"resource": self._model_dump_without_embeddings(res),
94+
"items": [self._model_dump_without_embeddings(item) for item in items],
95+
"categories": [self._model_dump_without_embeddings(self.store.categories[c]) for c in cat_ids],
9596
"relations": [r.model_dump() for r in rels],
9697
}
9798

@@ -110,7 +111,7 @@ async def _create_resource_with_caption(
110111
caption_text = caption.strip()
111112
if caption_text:
112113
res.caption = caption_text
113-
res.caption_embedding = (await self.openai.embed([caption_text]))[0]
114+
res.embedding = (await self.openai.embed([caption_text]))[0]
114115
return res
115116

116117
def _resolve_memory_types(self) -> list[MemoryType]:
@@ -365,6 +366,11 @@ def _extract_json_blob(raw: str) -> str:
365366
def _escape_prompt_value(value: str) -> str:
366367
return value.replace("{", "{{").replace("}", "}}")
367368

369+
def _model_dump_without_embeddings(self, obj: BaseModel) -> dict[str, Any]:
370+
data = obj.model_dump()
371+
data.pop("embedding", None)
372+
return data
373+
368374
async def retrieve(self, query: str, *, top_k: int = 5) -> dict[str, Any]:
369375
qvec = (await self.openai.embed([query]))[0]
370376
response: dict[str, list[dict[str, Any]]] = {"resources": [], "items": [], "categories": []}
@@ -413,7 +419,7 @@ def _materialize_hits(self, hits: Sequence[tuple[str, float]], pool: dict[str, A
413419
obj = pool.get(_id)
414420
if not obj:
415421
continue
416-
data = obj.model_dump()
422+
data = self._model_dump_without_embeddings(obj)
417423
data["score"] = float(score)
418424
out.append(data)
419425
return out
@@ -450,8 +456,8 @@ def _format_resource_content(self, hits: list[tuple[str, float]]) -> str:
450456
def _resource_caption_corpus(self) -> list[tuple[str, list[float]]]:
451457
corpus: list[tuple[str, list[float]]] = []
452458
for rid, res in self.store.resources.items():
453-
if res.caption_embedding:
454-
corpus.append((rid, res.caption_embedding))
459+
if res.embedding:
460+
corpus.append((rid, res.embedding))
455461
return corpus
456462

457463
async def _judge_retrieval_sufficient(self, query: str, content: str) -> bool:

src/memu/app/settings.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,12 @@ class AppSettings(BaseModel):
3232
resources_dir: str = Field(default="./resources")
3333
# openai base
3434
openai_base: str = Field(default="https://api.openai.com/v1")
35-
openai_api_key_env: str = Field(default="OPENAI_API_KEY")
35+
openai_api_key: str = Field(default="OPENAI_API_KEY")
3636
# models
37-
chat_model: str = Field(default="gpt-4o-mini")
37+
chat_model: str = Field(default="gpt-5-nano")
3838
embed_model: str = Field(default="text-embedding-3-small")
3939
llm_client_backend: str = Field(
40-
default="httpx",
40+
default="sdk",
4141
description="Which OpenAI client backend to use: 'httpx' (httpx) or 'sdk' (official OpenAI).",
4242
)
4343
llm_http_provider: str = Field(

src/memu/llm/backends/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class HTTPBackend:
1111
embedding_endpoint: str = "/embeddings"
1212

1313
def build_summary_payload(
14-
self, *, text: str, system_prompt: str | None, chat_model: str, max_tokens: int
14+
self, *, text: str, system_prompt: str | None, chat_model: str, max_tokens: int | None
1515
) -> dict[str, Any]:
1616
raise NotImplementedError
1717

src/memu/llm/backends/openai.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class OpenAIHTTPBackend(HTTPBackend):
1111
embedding_endpoint = "/embeddings"
1212

1313
def build_summary_payload(
14-
self, *, text: str, system_prompt: str | None, chat_model: str, max_tokens: int
14+
self, *, text: str, system_prompt: str | None, chat_model: str, max_tokens: int | None
1515
) -> dict[str, Any]:
1616
prompt = system_prompt or "Summarize the text in one short paragraph."
1717
return {

src/memu/llm/http_client.py

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
from __future__ import annotations
22

3-
import os
3+
import logging
44
from collections.abc import Callable
5-
from typing import cast
65

76
import httpx
8-
import numpy as np
97

108
from memu.llm.backends.base import HTTPBackend
119
from memu.llm.backends.openai import OpenAIHTTPBackend
1210

11+
logger = logging.getLogger(__name__)
12+
1313
HTTP_BACKENDS: dict[str, Callable[[], HTTPBackend]] = {
1414
OpenAIHTTPBackend.name: OpenAIHTTPBackend,
1515
}
@@ -41,46 +41,31 @@ def __init__(
4141
or overrides.get("embed")
4242
or self.backend.embedding_endpoint
4343
)
44-
self.fake = bool(os.getenv("MEMUFLOW_FAKE_OPENAI")) or not bool(self.api_key)
4544
self.timeout = timeout
4645

47-
async def summarize(self, text: str, max_tokens: int = 160, system_prompt: str | None = None) -> str:
48-
if self.fake:
49-
s = " ".join(text.strip().split())
50-
return s[:200] + ("..." if len(s) > 200 else "")
51-
46+
async def summarize(self, text: str, max_tokens: int | None = None, system_prompt: str | None = None) -> str:
5247
payload = self.backend.build_summary_payload(
5348
text=text, system_prompt=system_prompt, chat_model=self.chat_model, max_tokens=max_tokens
5449
)
5550
async with httpx.AsyncClient(base_url=self.base_url, timeout=self.timeout) as client:
5651
resp = await client.post(self.summary_endpoint, json=payload, headers=self._headers())
5752
resp.raise_for_status()
5853
data = resp.json()
54+
logger.debug("HTTP LLM summarize response: %s", data)
5955
return self.backend.parse_summary_response(data)
6056

6157
async def embed(self, inputs: list[str]) -> list[list[float]]:
62-
if self.fake:
63-
return [self._fake_vec(x) for x in inputs]
6458
payload = self.backend.build_embedding_payload(inputs=inputs, embed_model=self.embed_model)
6559
async with httpx.AsyncClient(base_url=self.base_url, timeout=self.timeout) as client:
6660
resp = await client.post(self.embedding_endpoint, json=payload, headers=self._headers())
6761
resp.raise_for_status()
6862
data = resp.json()
63+
logger.debug("HTTP LLM embedding response: %s", data)
6964
return self.backend.parse_embedding_response(data)
7065

7166
def _headers(self) -> dict[str, str]:
7267
return {"Authorization": f"Bearer {self.api_key}"}
7368

74-
def _fake_vec(self, s: str, dim: int = 256) -> list[float]:
75-
import hashlib
76-
77-
h = hashlib.sha256(s.encode("utf-8")).digest()
78-
b = (h * (dim // len(h) + 1))[:dim]
79-
arr = np.frombuffer(b, dtype=np.uint8).astype(np.float32)
80-
arr = (arr - arr.mean()) / (arr.std() + 1e-6)
81-
arr = arr / (np.linalg.norm(arr) + 1e-9)
82-
return cast(list[float], arr.tolist())
83-
8469
def _load_backend(self, provider: str) -> HTTPBackend:
8570
factory = HTTP_BACKENDS.get(provider)
8671
if not factory:

src/memu/llm/openai_sdk.py

Lines changed: 10 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
1-
import os
2-
from typing import TYPE_CHECKING, cast
1+
import logging
2+
from typing import cast
33

4-
import numpy as np
4+
from openai import AsyncOpenAI
55

6-
if TYPE_CHECKING:
7-
from openai import AsyncOpenAI # 只给类型检查用
8-
9-
try:
10-
import openai
11-
except ImportError:
12-
openai = None # 运行时用来判断有没有这个库
6+
logger = logging.getLogger(__name__)
137

148

159
class OpenAISDKClient:
@@ -20,58 +14,30 @@ def __init__(self, *, base_url: str, api_key: str, chat_model: str, embed_model:
2014
self.api_key = api_key or ""
2115
self.chat_model = chat_model
2216
self.embed_model = embed_model
23-
self.fake = bool(os.getenv("MEMUFLOW_FAKE_OPENAI")) or not bool(self.api_key)
24-
self.client: AsyncOpenAI | None = None
25-
if self.fake:
26-
self.client = None
27-
else:
28-
if openai is None:
29-
msg = "The 'openai' Python package is required for the SDK client. Install it via `pip install openai` or switch to the httpx backend."
30-
raise RuntimeError(msg)
31-
self.client = openai.AsyncOpenAI(api_key=self.api_key, base_url=self.base_url)
17+
self.client = AsyncOpenAI(api_key=self.api_key, base_url=self.base_url)
3218

3319
async def summarize(
3420
self,
3521
text: str,
3622
*,
37-
max_tokens: int = 160,
23+
max_tokens: int | None = None,
3824
system_prompt: str | None = None,
3925
) -> str:
4026
prompt = system_prompt or "Summarize the text in one short paragraph."
41-
if self.fake:
42-
s = " ".join(text.strip().split())
43-
return s[:200] + ("..." if len(s) > 200 else "")
44-
if self.client is None:
45-
msg = "The 'openai' Python package is required for the SDK client. Install it via `pip install openai` or switch to the httpx backend."
46-
raise RuntimeError(msg)
27+
4728
response = await self.client.chat.completions.create(
4829
model=self.chat_model,
4930
messages=[
5031
{"role": "system", "content": prompt},
5132
{"role": "user", "content": text},
5233
],
53-
temperature=0.2,
54-
max_tokens=max_tokens,
34+
temperature=1,
35+
max_completion_tokens=max_tokens,
5536
)
5637
content = response.choices[0].message.content
38+
logger.debug("OpenAI summarize response: %s", response)
5739
return content or ""
5840

5941
async def embed(self, inputs: list[str]) -> list[list[float]]:
60-
if self.fake:
61-
return [self._fake_vec(x) for x in inputs]
62-
if self.client is None:
63-
msg = "The 'openai' Python package is required for the SDK client. Install it via `pip install openai` or switch to the httpx backend."
64-
raise RuntimeError(msg)
6542
response = await self.client.embeddings.create(model=self.embed_model, input=inputs)
6643
return [cast(list[float], d.embedding) for d in response.data]
67-
68-
def _fake_vec(self, s: str, dim: int = 256) -> list[float]:
69-
# deterministic pseudo-embedding for offline demo
70-
import hashlib
71-
72-
h = hashlib.sha256(s.encode("utf-8")).digest()
73-
b = (h * (dim // len(h) + 1))[:dim]
74-
arr = np.frombuffer(b, dtype=np.uint8).astype(np.float32)
75-
arr = (arr - arr.mean()) / (arr.std() + 1e-6)
76-
arr = arr / (np.linalg.norm(arr) + 1e-9)
77-
return arr.tolist()

src/memu/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class Resource(BaseModel):
1313
modality: str
1414
local_path: str
1515
caption: str | None = None
16-
caption_embedding: list[float] | None = None
16+
embedding: list[float] | None = None
1717

1818

1919
class MemoryItem(BaseModel):

0 commit comments

Comments
 (0)