|
13 | 13 |
|
14 | 14 | Naming: the nine methods 1.0.0 shipped are bare verbs and stay that way — ``add`` / |
15 | 15 | ``search`` / ``get`` / ``flush`` / ``edit`` / ``delete`` (memory), ``presign`` / |
16 | | -``upload`` (storage), ``close``. They are public API and cannot be renamed. Everything |
17 | | -added since carries its resource as a prefix — ``kb_`` / ``doc_`` / ``task_`` / |
18 | | -``tag_`` — so completion groups by resource and no facade method collides with the |
19 | | -generated method it wraps. The two styles sitting side by side is a consequence of that |
20 | | -freeze, not a convention worth copying. |
| 16 | +``upload`` (storage), ``close``. They are public API and cannot be renamed. Memory |
| 17 | +methods added since follow the same bare-verb shape so the memory group stays one |
| 18 | +style and each name is the last segment of its route — ``update`` / ``delete_by_ids`` |
| 19 | +/ ``feedback`` (1.2.0). Every other resource carries its name as a prefix — ``kb_`` / |
| 20 | +``doc_`` / ``task_`` / ``tag_`` — so completion groups by resource and no facade method |
| 21 | +collides with the generated method it wraps. |
21 | 22 |
|
22 | 23 | Errors: every failure raised by this facade derives from :class:`EverOSError` — |
23 | 24 | ``EverOSAPIError`` for HTTP errors, ``EverOSStorageError`` for object-upload failures, |
|
39 | 40 | AddOperation, |
40 | 41 | Content, |
41 | 42 | ContentItem, |
| 43 | + DeleteByIdsInput, |
42 | 44 | DeleteInput, |
43 | 45 | DeleteOperation, |
44 | 46 | DocIngestBody, |
45 | 47 | DocPatchBody, |
46 | 48 | EditInput, |
47 | 49 | EditInputOperationsInner, |
| 50 | + EpisodePatch, |
| 51 | + FeedbackInput, |
48 | 52 | FlushInput, |
49 | 53 | GetInput, |
50 | 54 | KbCreateInput, |
51 | 55 | KbPatchBody, |
52 | 56 | MessageItem, |
| 57 | + Reason, |
53 | 58 | SearchBody, |
54 | 59 | SearchInput, |
55 | 60 | SignObjectItem, |
56 | 61 | SignRequest, |
57 | 62 | TagBindInput, |
58 | 63 | TagReplaceInput, |
59 | 64 | TagUnbindInput, |
| 65 | + UpdateInput, |
60 | 66 | UpdateOperation, |
61 | 67 | ) |
62 | 68 |
|
|
80 | 86 | _FACADE_FOR = { |
81 | 87 | "add_memory": "add", "search_memory": "search", "get_memory": "get", |
82 | 88 | "flush_memory": "flush", "edit_profile": "edit", "delete_memory": "delete", |
| 89 | + "update_memory": "update", "delete_memories_by_ids": "delete_by_ids", |
| 90 | + "submit_feedback": "feedback", |
83 | 91 | "sign_objects": "presign", |
84 | 92 | "bind_tags": "tag_bind", "unbind_tags": "tag_unbind", "replace_tags": "tag_replace", |
85 | 93 | "create_knowledge_base": "kb_create", "list_knowledge_bases": "kb_list", |
|
106 | 114 |
|
107 | 115 | MessageLike = Union[MessageItem, Mapping[str, Any]] |
108 | 116 | ContentLike = Union[ContentItem, Mapping[str, Any], str] |
| 117 | +ReasonLike = Union[Reason, Mapping[str, Any], str] |
109 | 118 |
|
110 | 119 |
|
111 | 120 | def _guess_file_type(name: str) -> str: |
@@ -273,6 +282,15 @@ def _to_operation(op: Any) -> EditInputOperationsInner: |
273 | 282 | raise ValueError(f"unknown edit operation action: {op.get('action')!r}") |
274 | 283 | return EditInputOperationsInner(cls(**op)) |
275 | 284 |
|
| 285 | + @staticmethod |
| 286 | + def _to_reason(reason: ReasonLike | None) -> Reason | None: |
| 287 | + """``"redundant"`` / ``{"code": ..., "note": ...}`` / ``Reason`` -> ``Reason``.""" |
| 288 | + if reason is None or isinstance(reason, Reason): |
| 289 | + return reason |
| 290 | + if isinstance(reason, str): |
| 291 | + return Reason(code=reason) |
| 292 | + return Reason(**reason) |
| 293 | + |
276 | 294 | @staticmethod |
277 | 295 | def _to_content(content: ContentLike) -> ContentItem: |
278 | 296 | """Coerce a document body into a ``ContentItem``. |
@@ -422,6 +440,94 @@ def delete( |
422 | 440 | ) |
423 | 441 | return self._call(self.memory.delete_memory, payload).data |
424 | 442 |
|
| 443 | + # -- single-memory edits ------------------------------------------------- |
| 444 | + # These target memories by id, so like the tag calls they carry NO app_id / |
| 445 | + # project_id and `_scope` does not apply. Facade names are the last segment of |
| 446 | + # the route: /memory/update -> update, /memory/delete_by_ids -> delete_by_ids, |
| 447 | + # /memory/feedback -> feedback. |
| 448 | + def update( |
| 449 | + self, |
| 450 | + memory_id: str, |
| 451 | + *, |
| 452 | + episode: str | None = None, |
| 453 | + summary: str | None = None, |
| 454 | + subject: str | None = None, |
| 455 | + reason: ReasonLike | None = None, |
| 456 | + memory_type: str = "episode", |
| 457 | + ) -> Any: |
| 458 | + """Patch one episode's ``episode`` text, ``summary`` and/or ``subject``. |
| 459 | +
|
| 460 | + Only the fields you pass change (last write wins). ``reason`` is a code such as |
| 461 | + ``"wrong_subject"`` or ``{"code": ..., "note": ...}``. Returns ``UpdateData``, |
| 462 | + whose ``unchanged`` is true when the patch matched what was stored. |
| 463 | +
|
| 464 | + Profile items are edited with :meth:`edit`, not here. |
| 465 | + """ |
| 466 | + patch = _clean(episode=episode, summary=summary, subject=subject) |
| 467 | + if not patch: |
| 468 | + raise ValueError("update needs at least one of episode / summary / subject") |
| 469 | + payload = UpdateInput( |
| 470 | + **_clean( |
| 471 | + memory_type=memory_type, |
| 472 | + memory_id=memory_id, |
| 473 | + patch=EpisodePatch(**patch), |
| 474 | + reason=self._to_reason(reason), |
| 475 | + ) |
| 476 | + ) |
| 477 | + return self._call(self.memory.update_memory, payload).data |
| 478 | + |
| 479 | + def delete_by_ids( |
| 480 | + self, |
| 481 | + memory_ids: Sequence[str], |
| 482 | + *, |
| 483 | + reason: ReasonLike | None = None, |
| 484 | + memory_type: str = "episode", |
| 485 | + ) -> Any: |
| 486 | + """Soft-delete 1-50 memories by id. Returns ``DeleteByIdsData``. |
| 487 | +
|
| 488 | + Ids that do not exist are skipped; one malformed id fails the whole batch (422). |
| 489 | + To clear a whole user / agent / session, use :meth:`delete`. |
| 490 | + """ |
| 491 | + payload = DeleteByIdsInput( |
| 492 | + **_clean( |
| 493 | + memory_type=memory_type, |
| 494 | + memory_ids=list(memory_ids), |
| 495 | + reason=self._to_reason(reason), |
| 496 | + ) |
| 497 | + ) |
| 498 | + return self._call(self.memory.delete_memories_by_ids, payload).data |
| 499 | + |
| 500 | + def feedback( |
| 501 | + self, |
| 502 | + memory_id: str, |
| 503 | + rating: str, |
| 504 | + *, |
| 505 | + reason: str | None = None, |
| 506 | + note: str | None = None, |
| 507 | + suggestion: str | None = None, |
| 508 | + item_id: str | None = None, |
| 509 | + memory_type: str = "episode", |
| 510 | + ) -> Any: |
| 511 | + """Rate a memory ``"positive"`` or ``"negative"``. Returns ``FeedbackData``. |
| 512 | +
|
| 513 | + ``reason`` (a code such as ``"outdated"``), ``note`` and ``suggestion`` go with a |
| 514 | + negative rating only. ``item_id`` names the item inside a profile memory. This |
| 515 | + only records the rating — nothing is edited; use :meth:`update` / |
| 516 | + :meth:`delete_by_ids` to change the memory itself. |
| 517 | + """ |
| 518 | + payload = FeedbackInput( |
| 519 | + **_clean( |
| 520 | + memory_type=memory_type, |
| 521 | + memory_id=memory_id, |
| 522 | + item_id=item_id, |
| 523 | + rating=rating, |
| 524 | + reason=reason, |
| 525 | + note=note, |
| 526 | + suggestion=suggestion, |
| 527 | + ) |
| 528 | + ) |
| 529 | + return self._call(self.memory.submit_feedback, payload).data |
| 530 | + |
425 | 531 | # -- memory tags --------------------------------------------------------- |
426 | 532 | # Tag calls carry NO app_id / project_id: the scope of a tag operation is the |
427 | 533 | # memory ids themselves, so `_scope` deliberately does not apply here. |
|
0 commit comments