Python SDK

Python - 모든 메서드, 세 그룹.

쓰고, 읽고, 지우고. 아래 모든 예제는 2026-08-01 라이브 API에서 실제로 실행했고, 응답은 실물 그대로입니다.

pip install wontopos
from wontopos import Client

mem = Client(api_key="wos-live-...")  # or read from an env var

모델 선택

API 키는 어느 기억(당신 계정)을, 모델은 어느 엔진이 그 기억을 읽을지 정합니다. 모든 모델이 기억을 공유하므로, 한 모델로 저장하고 다른 모델로 회수할 수 있습니다. 클라이언트에 기본값을 두고, model=로 호출마다 바꾸세요.

mem = Client(api_key="wos-live-...", model="tablet-1")  # default engine
mem.recall("...", user_id="alice")                  # tablet-1
mem.recall("...", user_id="alice", model="scroll-1")  # or pick a model per call

list_models

카탈로그 - model에 넣을 수 있는 id와 각 모델의 가용 여부. memory: "shared"는 같은 저장소를, "isolated"는 자기 저장소를 씁니다. API 키가 필요 없습니다.

mem.list_models()
실제 응답
[{"id": "tablet-1", "name": "Tablet 1", "available": true, "memory": "shared"},
 {"id": "tablet-2", "name": "Tablet 2", "available": true, "memory": "shared"},
 {"id": "scroll-1", "name": "Scroll 1", "available": true, "memory": "shared"},
 {"id": "scroll-1.2", "name": "Scroll 1.2", "available": true, "memory": "shared"}]

ping

연결과 API 키가 유효한지 한 줄로 확인합니다.

mem.ping()   # True, or raises AuthenticationError / PaymentRequiredError

위 카탈로그는 항상 지금 쓸 수 있는 모델만 보여줍니다 - 다른 id를 넣으면 명확한 에러가 돌아옵니다. 새 모델은 출시되면 자동으로 거기에 나타납니다.

쓰기

add

기억 하나를 저장합니다. 적재에 LLM 호출이 없어, 쓰기 요금만 듭니다.

mem.add("she prefers tea over coffee", user_id="alice")
mem.add("I promised the summary by Friday", user_id="alice", speaker="me")  # its own words - no registration needed
실제 응답
{"id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "stored (1 chunks)"}

add_turn

대화 한 턴(사용자 + 어시스턴트)을 단기·장기 기억에 한 번에 저장합니다.

mem.add_turn("hi", "hello!", user_id="alice")
실제 응답
{"status": "ok"}

speaker

모든 기억에 누가 한 말인지 담을 수 있습니다. 사람은 한 번 등록하고, 그다음부터 이름을 speaker로 넘기세요. "me"(어시스턴트 자신의 말)는 등록이 필요 없습니다. 검색에도 speaker를 주면 그 사람의 말만 회수합니다.

mem.add_speaker("Bob", user_id="alice")  # once per person; "me" needs no registration
mem.add("I promised to send the report on Friday", user_id="alice", speaker="me")
mem.add("Bob said the deadline moved to Tuesday", user_id="alice", speaker="Bob")
mem.search("what did Bob say about deadlines?", user_id="alice", speaker="Bob")
response
[{"content": "Bob said the deadline moved to Tuesday", "speaker": "Bob", ...}]
화자는 저장소처럼 명시적입니다. 사람을 먼저 등록하고 그 이름으로 저장합니다. 오타가 조용히 새 사람이 되는 일이 없습니다. 저장소당 시작 기준 50명까지 등록되고(차차 늘릴 예정), "me"는 등록도 카운트도 필요 없습니다.

add_bulk

긴 텍스트를 한 번에 적재합니다. 서버에서 나누어 색인합니다 - 기존 히스토리 이관에 적합합니다.

mem.add_bulk("Alice moved to Brooklyn in March. She works at a design studio downtown.", user_id="alice")
실제 응답
{"elapsed_secs": 0.154154944, "status": "ok", "stored": 1, "total_chunks": 1}

update

사실이 바뀌었을 때. 옛 기억은 superseded 로 마킹되어 보존되고, 새 기억이 회수에서 그 자리를 차지합니다.

mem.update("576700aa-...", "she switched to coffee this year", user_id="alice")
실제 응답
{"new_memory_id": "07e94433-b7cc-4e49-8d8f-f37fc1a392b7",
 "old_memory_id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "superseded"}

읽기

search

의미 검색, 관련도 순. 어떤 언어로 물어도, 어떤 언어로 적힌 기억이든 찾습니다. SDK는 memories 배열을 바로 돌려주며, 아래는 HTTP 원문입니다. 어떤 모델은 결과 묶음을 둘 이상으로 답하고 SDK가 그것을 합쳐 돌려주므로, 배열이 max_results보다 많을 수 있습니다. 요청한 숫자가 아니라 받은 배열을 기준으로 프롬프트 크기를 잡으십시오.

r = mem.search("what does she drink?", user_id="alice", limit=1)
실제 응답 (HTTP 원문)
[{
   "id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624",
   "content": "she prefers tea over coffee",
   "category": "general",
   "time_bucket": "2026-06",
   "importance": 0.3,
   "similarity": 0.6316057443618774,
   "is_superseded": false,
   "superseded_by": null,
   "created_at": "2026-07-10T04:20:39.688276876Z"
 }]
필드의미
similarity질문과 이 기억이 얼마나 가까운지 (0–1).
is_supersededupdate()로 교체된 기억이면 true.
search_ms서버 검색 소요 시간.

recall

한 번의 왕복으로 LLM에 필요한 모든 것을 돌려줍니다 - 결과를 프롬프트에 그대로 넣으면 됩니다. 저장량과 무관하게 항상 고정 크기입니다.

ctx = mem.recall("what does she drink?", user_id="alice")
실제 응답 (구조 - 목록 축약)
{"short_term":  {"count": 2, "turns": [{"role": "user", "content": "hi", ...}]},
 "long_term":   {"count": 4, "memories": [{"content": "she prefers tea over coffee",
                                           "similarity": 0.63, ...}]},
 "context":     {"count": 4, "around_top_memory": [
                  "[match] she prefers tea over coffee",
                  "[after] Alice moved to Brooklyn in March. ..."]},
 "instruction": "Use short_term for recent context, long_term for relevant
                 past memories, context for surrounding conversation of the
                 most relevant memory."}

history

최근 대화 턴(단기 기억), 오래된 것부터.

turns = mem.history("alice")
실제 응답 (HTTP 원문)
{"count": 2, "turns": [
   {"role": "user",      "content": "hi",     "timestamp": "2026-07-10T04:20:40.989011337Z"},
   {"role": "assistant", "content": "hello!", "timestamp": "2026-07-10T04:20:40.989013416Z"}
 ], "user_id": "alice"}

stats

한 사용자의 기억 통계.

mem.stats("alice")
실제 응답
{"short_term_turns": 2, "total_memories": 4, "user_id": "alice"}

get

id로 기억 하나를 조회합니다 - add나 list_memories가 돌려준 그 id입니다. 저장한 원문과 메타데이터만 반환합니다. 다른 스토어의 id나 삭제·무효화된 기억은 404입니다.

m = mem.get("alice", memory_id="576700aa-...")
response
{"id": "576700aa-...", "content": "she prefers tea over coffee",
 "category": "general", "created_at": "2026-07-10T04:20:39Z", "event_date": null,
 "is_superseded": false, "superseded_by": null}
실제 응답
{"memory": {"id": "8bd090de-...", "content": "the office moved to the seventh floor in June",
  "category": "general", "created_at": "2026-07-31T18:20:30.531518060+00:00", "event_date": null,
  "is_superseded": false, "superseded_by": null}, "user_id": "docs_livetest"}

list_memories

스토어에 저장된 기억을 나열합니다. 저장한 원문과 메타데이터만 반환합니다. 커서로 페이지를 넘깁니다 - 응답의 next_cursor를 다음 호출에 넘기면 됩니다.

page = mem.list_memories("alice", limit=100)
response
{"count": 2, "next_cursor": null, "memories": [
   {"id": "576700aa-...", "content": "she prefers tea over coffee",
    "category": "general", "created_at": "2026-07-10T04:20:39Z", "event_date": null,
 "is_superseded": false, "superseded_by": null}
 ]}

iter_memories · export_memories

커서를 직접 관리하지 않고 모든 기억을 순회하거나, 스토어 전체를 한 번에 가져옵니다.

for m in mem.iter_memories("alice"):   # every page, no cursor bookkeeping
    print(m["id"], m["content"])
everything = mem.export_memories("alice")   # the whole store as a list

삭제

delete

기억 하나를 id로 삭제합니다.

mem.delete("alice", memory_id="576700aa-...")
실제 응답
{"memory_id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "deleted"}

delete_all

한 사용자의 모든 기억을 삭제 - 한 번의 호출, GDPR 대응.

mem.delete_all("alice")
실제 응답
{"memories_deleted": 4, "status": "deleted", "user_id": "alice"}

오류와 신뢰성

모든 실패는 타입이 있는 오류입니다. 상황별(rate limit·인증·결제)로 골라 잡거나, 기본 WosError로 한꺼번에 잡습니다.

from wontopos import PaymentRequiredError, NotFoundError
try:
    mem.add("...", user_id="alice")
except NotFoundError:
    mem.create_store("alice")   # store didn't exist yet
except PaymentRequiredError:
    top_up()                       # out of credit - don't retry

rate_limit

호출 직후 남은 한도를 읽어, 한계에 닿기 전에 속도를 늦춥니다.

mem.search("...", user_id="alice")
rl = mem.rate_limit   # {"limit": 150, "remaining": 3, "reset": ...}

search_self

자기기억 모델(Scroll 1.2+)에서 한 번의 호출로 두 갈래를 받습니다: 남이 한 말과 에이전트 자신이 한 말을 따로 돌려주므로, 읽는 쪽이 누가 말했는지 헷갈리지 않습니다.

r = mem.search_self("what did I promise?", user_id="alice")
r["memories"]       # what others said / general memories
r["self_memories"]  # the agent's OWN words (speaker "me")

list_engrams

이 모델이 실행할 수 있는 엔그램과 딜리버리 폼을 서비스에 물어봅니다. 이름을 하드코딩하면 새 엔그램이 나온 순간부터 그 코드에는 영영 보이지 않습니다.

cat = mem.list_engrams()
[e["name"] for e in cat["engrams"]]   # ask, never hard-code

filters

검색을 저장소의 일부로 좁힙니다. 랭킹 전에 걸리므로 필터 안에서의 최선이 나옵니다 - 상위 N 을 걸러낸 것이 아닙니다.

mem.search("what did we decide", user_id="alice", filters={
    "categories": ["work"],
    "event_from": "2026-01-01",   # when it HAPPENED
})
키: categories · event_from / event_to (내용이 언제 일어났나 - metadata.event_date) · time_from / time_to (언제 적재됐나) · min_importance. 목록에 없는 키는 거부가 아니라 버려지므로, 오타는 조용히 검색을 넓힙니다.

idempotency_key

쓰기 하나를 다시 보내도 안전하게 만듭니다. 재시도가 내 쪽에서 일어날 때 씁니다 - 죽었다 다시 돈 작업, 다시 전달하는 큐.

mem.add("she prefers tea", "alice", idempotency_key=f"import:{row.id}")
키는 저장하려는 대상에서 뽑아 만드세요(import:row-42). 상수를 쓰면 안 됩니다 - 서로 다른 두 쓰기에 같은 키를 쓰면 첫 응답이 재생되고 두 번째 쓰기는 조용히 사라집니다. 형식: [A-Za-z0-9._:-] 1~128 자.

with_timeout / with_retries / with_deadline

이미 만들어 둔 클라이언트를 건드리지 않고 호출 지점 하나만 조정합니다. 큰 백필에는 타임아웃이 긴 복제본을, 직접 재시도 루프를 돌 때는 재시도를 끈 복제본을 씁니다.

timeout은 시도 하나를 묶습니다. 그래서 재시도하는 호출은 그보다 오래 걸릴 수 있습니다 — 기본값이면 한 번의 호출이 30초를 붙들고, 물러섰다가, 다시 시도하고, 또 시도합니다. deadline은 대신 호출 전체를 묶습니다. 매 시도가 남은 시간만큼으로 잘리고, 물러서는 시간도 예산을 넘겨 자지 않습니다. 호출하는 쪽에 실제 한도가 있을 때 — 5초짜리 요청 핸들러 같은 곳에 — 설정하세요.

mem.with_timeout(120).add_bulk(big_blob, "alice")  # this slow call only
mem.with_retries(0).add("...", "alice")              # you retry, not the SDK
mem.with_deadline(5).recall("...", "alice")             # 5s for the whole call