Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/en/docs/how-to/full-capability-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,5 @@ ID remain stable because they are persisted in the database.
| Existing data is missing | Restore the previous database URL or `POWERCONTEXT_HOME` |

See [Troubleshooting](troubleshoot.md) and [Configuration](../reference/configuration.md) for details.

To organize saved Artifacts and individual Memory entries, see [Custom tags](manage-artifact-tags.md).
132 changes: 132 additions & 0 deletions docs/en/docs/how-to/manage-artifact-tags.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
---
title: Organize Artifacts with custom tags
description: Label logical Artifacts and Memory entries, then find them with exact tag filters.
---

# Organize Artifacts with custom tags

Custom tags organize Memory, Experience, Skill, and Handoff Artifacts within one Scope. A Memory Artifact and each
logical entry inside it have independent tag sets. Tags follow these identities across content revisions; they do not
change content, lineage, embeddings, or Context Versions.

With access control enabled, tags follow their target's read and write permissions. A viewer of a shared target can read
its tags but cannot edit them or run a Scope-wide tag query. Queries require `scope.read`. Tags on the entire Memory
Artifact require `scope.read` to read and `scope.admin` to edit; individual entries use their own `artifact.read` /
`artifact.write` permissions. Insufficient permission returns **403**, and revoking a share also revokes tag access.

## Use the Dashboard

Start the Server and open its Overview page. In **Custom tags**:

1. Select the exact Scope, target kind, family, and Artifact. For a Memory entry, also select its entry ID.
2. Enter one label per line and select **Save tags**. Saving an empty field clears that target's labels.
3. Enter labels under **Find by exact labels**. Choose **All** or **Any**, then select **Find targets**.
4. Select a result to edit its tags. **Include inactive** also returns inactive Memory entries and deprecated or retired
Artifacts.

If another writer changes the labels, saving displays a conflict and preserves your input. Use **Reload tags** to read
the current state before deciding what to save. Reloading replaces the input field; copy any text you want to retain first.

## Use the Python Client

You need a running Server and an existing Scope and Artifact. Take their IDs from the Dashboard or the corresponding
Scope and Artifact APIs; a title is not an ID. Set these non-secret example variables in your terminal:

```bash
export POWERCONTEXT_TAG_SCOPE='your-existing-scope-id'
export POWERCONTEXT_TAG_FAMILY='skill'
export POWERCONTEXT_TAG_ARTIFACT='your-existing-artifact-id'
```

Run this with `powercontext` installed. If the Server requires authentication, provide its bearer token through
`POWERCONTEXT_SERVER_AUTH_TOKEN`; do not embed it in the script.

```python
import asyncio
import os

from powercontext.client import PowerContextClient
from powercontext.http import QueryArtifactTagsRequest, ReplaceArtifactTagsRequest


async def main():
scope = os.environ["POWERCONTEXT_TAG_SCOPE"]
family = os.environ["POWERCONTEXT_TAG_FAMILY"]
artifact = os.environ["POWERCONTEXT_TAG_ARTIFACT"]
async with PowerContextClient(
"http://127.0.0.1:8000", token=os.getenv("POWERCONTEXT_SERVER_AUTH_TOKEN")
) as client:
current = await client.get_artifact_tags(scope, family, artifact)
if current is None:
raise RuntimeError("An unconditional read must return the current tag set")
saved = await client.replace_artifact_tags(
scope, family, artifact,
ReplaceArtifactTagsRequest.model_validate({"tags": ["customer-a", "release"]}),
expected_etag=current.etag,
)
print(saved.tag_set.model_dump(mode="json")["tags"])
matches = await client.query_artifact_tags(
scope, QueryArtifactTagsRequest.model_validate({"tags": ["CUSTOMER-A"]})
)
print([item.target.model_dump(mode="json") for item in matches.items])


asyncio.run(main())
```

The output contains the two saved labels and a matching target. For an entry, use `get_memory_entry_tags` and
`replace_memory_entry_tags` with `(scope_id, artifact_id, entry_id)`. Read the entry ID from the current Memory manifest
or a Memory citation, not from `entry_version_id`. A Scope can hold multiple Memory Artifacts; the existing scoped
Memory list and search operations address the runtime's designated Memory.

## HTTP and retrieval filters

| Method | Path | Purpose |
| --- | --- | --- |
| GET / PUT | `/v1/scopes/{scope_id}/artifacts/{family}/{artifact_id}/tags` | Read or replace an Artifact's labels |
| GET / PUT | `/v1/scopes/{scope_id}/artifacts/memory/{artifact_id}/entries/{entry_id}/tags` | Read or replace a logical entry's labels |
| POST | `/v1/scopes/{scope_id}/artifact-tags/query` | Find tagged targets across families |

PUT accepts `{"tags":["customer-a","release"]}` and requires the ETag returned by GET in `If-Match`. Missing `If-Match`
returns **428**; stale or wrong-target state returns **412**. An unchanged conditional GET returns **304**.
`tag_digest` describes the canonical tag set, but is not the HTTP mutation precondition.

Artifact listing accepts repeated `tag` parameters and optional `tag_match=all|any`, for example
`/v1/scopes/{scope_id}/artifacts/skill?tag=release&tag=customer-a&tag_match=all`.
Supplying `tag_match` without `tag` is invalid.

Memory entry listing and search accept this optional request field:

```json
{"tag_filter":{"tags":["customer-a","release"],"match":"all"}}
```

Search filters entry tags, not the parent Memory Artifact's tags, and still returns only active entries. Full-text and
vector candidates are filtered in the database before candidate limits, fusion, and reranking. Tagged vector queries
use exact distance ordering over the eligible set on SQLite and OceanBase; this can cost more than an unfiltered
approximate search. A backend without tag-filter support rejects the request instead of silently post-filtering.

Tag queries return exact current Artifact references or Memory citations, ordered by family, target type, Artifact ID,
and target ID. Pass `next_cursor` unchanged with the same filters, Scope, and caller. Cursors expire after one hour;
invalid or mismatched cursors return **400**, expired cursors **410**. Each page is internally consistent, but pagination
does not freeze a snapshot across requests.

## Label rules and storage

- A target holds at most 32 labels; a filter accepts 1–16 labels.
- Each label has 1–64 Unicode code points, no outer whitespace, and no control, surrogate, or unassigned characters.
- Matching uses NFC normalization followed by Unicode case folding. The submitted display spelling is retained.
Normalized duplicates such as `Straße` and `STRASSE` are rejected atomically. A normalized key may not exceed 128
code points.
- Tags are Scope-local discovery metadata, not permissions or trusted instructions. They are not added to model prompts,
Skill package frontmatter, or publication/import payloads. Published copies start without the source target's tags.
- Inactive entries remain taggable while present in the current authoritative manifest. Rebuilding active search
projections does not remove their labels.

All assignments live in `pc_artifact_tags`, with a foreign key to the owning Artifact head. The table retains the full
normalized key and indexes a 32-byte SHA-256 key fingerprint. This preserves the parent column lengths required by
OceanBase while keeping composite indexes below its 3072-byte limit. Matching checks both fingerprint and complete key.
Identical replacement preserves assignment timestamps. Existing Artifacts need no backfill; their initial tag sets are
empty. Include the table in backups and restore it after Artifact heads.

See the Server's [HTTP API reference](/api) for complete request and response schemas.
2 changes: 1 addition & 1 deletion docs/en/docs/how-to/troubleshoot.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ so the previous database remains available for recovery:

```bash
obloader <connection-options> -D <new-database> --csv \
--table 'pc_artifact_candidate_heads,pc_memory_entry_heads' \
--table 'pc_artifact_candidate_heads,pc_memory_entry_heads,pc_artifact_tags' \
-f <export-directory>
```

Expand Down
2 changes: 2 additions & 0 deletions docs/zh/docs/how-to/full-capability-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,5 @@ Codex 启动后发送普通 prompt。插件从绑定 Scope 召回内容,并把
| 已有数据消失 | 恢复原数据库 URL 或 `POWERCONTEXT_HOME` |

更多信息见[故障排查](troubleshoot.md)和[配置](../reference/configuration.md)。

需要分类和检索制品或单条记忆时,参见[自定义标签](manage-artifact-tags.md)。
124 changes: 124 additions & 0 deletions docs/zh/docs/how-to/manage-artifact-tags.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
---
title: 用自定义标签管理制品
description: 为逻辑制品和记忆条目设置标签,并通过精确标签进行检索。
---

# 用自定义标签管理制品

Memory、Experience、Skill 和 Handoff 都可以在各自 Scope 内设置标签。一个 Memory 制品与其中的每条逻辑记忆分别拥有独立的标签集合。
标签跟随逻辑 ID,不会修改内容 Revision、条目版本、血缘、向量或 Context Version。

开启访问控制时,标签遵循所属对象的读取与修改权限。只读分享者可以读取该对象的标签,不能修改标签或执行 Scope 级标签查询。
跨对象查询需要 `scope.read`;Memory 制品整体标签需要 `scope.read` 才能读取、`scope.admin` 才能修改,
单条记忆的标签则使用该条目的 `artifact.read` / `artifact.write` 权限。权限不足返回 **403**,撤销分享后立即失去相应标签访问权限。

## 在 Dashboard 中使用

启动 Server,打开总览页的**自定义标签**面板:

1. 选择准确的 Scope、标签对象类型、制品类型和制品。对记忆条目,还需要选择 entry ID。
2. 每行输入一个标签,点击**保存标签**。清空输入框后保存,即可清除该对象的全部标签。
3. 在精确查找输入框中填写标签,选择**全部匹配**或**任一匹配**,点击**查找对象**。
4. 点击结果可以编辑对应对象。勾选**包含非活跃对象**,还能找到已停用的记忆条目和已弃用或退役的制品。

如果标签被其他操作修改,保存会提示冲突并保留输入。先点击**重新读取标签**,查看最新状态,再决定如何保存。
重新读取会替换输入框内容;需要保留的文字请先复制。

## 使用 Python Client

需要一个运行中的 Server,以及已有的 Scope 和制品。请从 Dashboard 或对应 API 获取 ID,不能用标题代替 ID。
在终端设置以下非敏感参数:

```bash
export POWERCONTEXT_TAG_SCOPE='已有的-scope-id'
export POWERCONTEXT_TAG_FAMILY='skill'
export POWERCONTEXT_TAG_ARTIFACT='已有的-artifact-id'
```

在已安装 `powercontext` 的环境中运行下面的代码。若 Server 开启认证,通过 `POWERCONTEXT_SERVER_AUTH_TOKEN` 提供 bearer token,
不要把凭据写进代码。

```python
import asyncio
import os

from powercontext.client import PowerContextClient
from powercontext.http import QueryArtifactTagsRequest, ReplaceArtifactTagsRequest


async def main():
scope = os.environ["POWERCONTEXT_TAG_SCOPE"]
family = os.environ["POWERCONTEXT_TAG_FAMILY"]
artifact = os.environ["POWERCONTEXT_TAG_ARTIFACT"]
async with PowerContextClient(
"http://127.0.0.1:8000", token=os.getenv("POWERCONTEXT_SERVER_AUTH_TOKEN")
) as client:
current = await client.get_artifact_tags(scope, family, artifact)
if current is None:
raise RuntimeError("An unconditional read must return the current tag set")
saved = await client.replace_artifact_tags(
scope, family, artifact,
ReplaceArtifactTagsRequest.model_validate({"tags": ["customer-a", "release"]}),
expected_etag=current.etag,
)
print(saved.tag_set.model_dump(mode="json")["tags"])
matches = await client.query_artifact_tags(
scope, QueryArtifactTagsRequest.model_validate({"tags": ["CUSTOMER-A"]})
)
print([item.target.model_dump(mode="json") for item in matches.items])


asyncio.run(main())
```

输出应包含保存的两个标签和匹配的对象。对记忆条目,使用 `get_memory_entry_tags` 与 `replace_memory_entry_tags`,
传入 `(scope_id, artifact_id, entry_id)`。entry ID 来自当前 manifest 或 Memory citation,不是 `entry_version_id`。
一个 Scope 可以有多个 Memory 制品;已有的 Scope 级记忆列表和检索接口操作的是运行时指定的 Memory。

## HTTP 接口与检索过滤

| Method | Path | 用途 |
| --- | --- | --- |
| GET / PUT | `/v1/scopes/{scope_id}/artifacts/{family}/{artifact_id}/tags` | 读取或替换制品标签 |
| GET / PUT | `/v1/scopes/{scope_id}/artifacts/memory/{artifact_id}/entries/{entry_id}/tags` | 读取或替换逻辑记忆条目的标签 |
| POST | `/v1/scopes/{scope_id}/artifact-tags/query` | 跨制品类型精确查找标签对象 |

PUT 请求体是 `{"tags":["customer-a","release"]}`,并且必须通过 `If-Match` 提交 GET 返回的 ETag。
缺少 `If-Match` 返回 **428**;标签状态过期或 ETag 属于其他对象,返回 **412**。
条件 GET 检测到标签未变化时返回 **304**。`tag_digest` 仅描述规范化的标签集合,不能代替 HTTP 写入使用的 ETag。

制品列表支持重复的 `tag` 参数,以及可选的 `tag_match=all|any`,例如:
`/v1/scopes/{scope_id}/artifacts/skill?tag=release&tag=customer-a&tag_match=all`。
没有 `tag` 时不能单独传入 `tag_match`。

记忆条目列表和检索请求支持以下可选字段:

```json
{"tag_filter":{"tags":["customer-a","release"],"match":"all"}}
```

记忆检索匹配的是条目自己的标签,不是所属 Memory 制品的标签,且仍然只返回活跃条目。
全文与向量通道都在数据库候选集阶段过滤,之后才应用候选数量限制、融合与重排。
SQLite 和 OceanBase 的带标签向量查询会对符合条件的集合进行精确距离排序,成本可能高于不带标签的近似搜索。
不支持标签过滤的后端会明确拒绝请求,不会静默改成先截断再过滤。

标签查询返回当前的精确 Artifact 引用或 Memory citation,按制品类型、对象类型、制品 ID、对象 ID 排序。
翻页时原样传回 `next_cursor`,并保持 Scope、过滤条件和调用方一致。游标有效期是一小时;无效或不匹配返回 **400**,
过期返回 **410**。单页内部保持一致,但跨页不固定数据库快照。

## 标签规则与存储

- 每个对象最多 32 个标签;一次过滤接受 1–16 个标签。
- 每个标签包含 1–64 个 Unicode 码点,首尾不能有空白,不能包含控制字符、代理字符或未分配字符。
- 匹配键由 NFC 规范化后再执行 Unicode case folding 得到,显示文字保留原始写法。
`Straße` 和 `STRASSE` 等规范化后重复的标签会让整次请求失败;规范化键不能超过 128 个码点。
- 标签是 Scope 内的检索元数据,不是权限或可信指令,不会进入模型提示词、Skill 包 frontmatter 或发布/导入内容。
发布后的副本不会继承源对象的标签。
- 只要条目仍在当前权威 manifest 中,即使已经停用也可以维护标签。重建活跃搜索投影不会删除标签。

所有关联都存储在 `pc_artifact_tags` 一张表中,通过外键关联所属制品的 head。
表内保留完整规范化键,并使用 32 字节 SHA-256 键摘要建立索引,以同时满足 OceanBase 的外键列长度要求和 3072 字节索引限制。
匹配时同时校验摘要与完整键。相同集合的重复替换保留关联时间。已有制品无需回填,初始标签为空;备份时应包含此表,
恢复时安排在 Artifact heads 之后。

完整请求与响应结构见 Server 的 [HTTP API 参考](/api)。
2 changes: 1 addition & 1 deletion docs/zh/docs/how-to/troubleshoot.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ collation,但不会包含数据库 URL 或凭据。

```bash
obloader <connection-options> -D <new-database> --csv \
--table 'pc_artifact_candidate_heads,pc_memory_entry_heads' \
--table 'pc_artifact_candidate_heads,pc_memory_entry_heads,pc_artifact_tags' \
-f <export-directory>
```

Expand Down
Loading
Loading