Files
xinmi/internal/modelcontext/handles.go
T
yawan f7fc992f57
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
2026-09-22 16:19:06 +08:00

97 lines
2.9 KiB
Go

package modelcontext
import (
"sort"
"strings"
)
// HandleTable is a typed, invocation-local bidirectional mapping used when a
// prompt needs compact handles for durable values (wiki issue iN handles,
// ingest ref-N slug handles, c000 citation-batch handles). Handles must never
// be persisted; Resolve converts model output back to the durable value first.
// It is the exported word-boundary-aware wrapper over handleTable.
type HandleTable struct {
table *handleTable[struct{}]
}
// NewHandleTable creates a handle space such as c000 (prefix=c, width=3,
// start=0) or ref-1 (prefix=ref-, width=0, start=1).
func NewHandleTable(prefix string, width, start int) *HandleTable {
return &HandleTable{table: newHandleTable[struct{}](prefix, width, start)}
}
// Register returns the stable handle assigned to value in this table.
func (t *HandleTable) Register(value string) string {
if t == nil {
return ""
}
value = strings.TrimSpace(value)
if value == "" {
return ""
}
return t.table.register(value, value, struct{}{}, nil)
}
// Handle returns an already registered handle without creating one.
func (t *HandleTable) Handle(value string) (string, bool) {
if t == nil {
return "", false
}
return t.table.handleForKey(value)
}
// Resolve converts a known model handle back to its durable value.
func (t *HandleTable) Resolve(handle string) (string, bool) {
if t == nil {
return "", false
}
value, _, ok := t.table.resolve(strings.TrimSpace(handle))
return value, ok
}
func (t *HandleTable) Empty() bool {
return t == nil || t.table.size() == 0
}
func (t *HandleTable) Len() int {
if t == nil {
return 0
}
return t.table.size()
}
// EncodeKnownText replaces already-registered durable values with their model
// handles. Longer values are processed first to avoid substring shadowing.
func (t *HandleTable) EncodeKnownText(value string) string {
if t == nil || value == "" {
return value
}
pairs := t.table.pairs()
sort.SliceStable(pairs, func(i, j int) bool { return len(pairs[i].value) > len(pairs[j].value) })
for _, item := range pairs {
value = strings.ReplaceAll(value, item.value, item.handle)
}
return value
}
// DecodeKnownText restores registered handles in complete text. Unlike the
// resource codec, replacement is word-boundary-aware so short handles such as
// i1 cannot fire inside ordinary prose tokens.
func (t *HandleTable) DecodeKnownText(value string) string {
if t == nil || value == "" {
return value
}
pairs := t.table.pairs()
sort.SliceStable(pairs, func(i, j int) bool { return len(pairs[i].handle) > len(pairs[j].handle) })
for _, item := range pairs {
if item.wordBounded == nil {
continue
}
// A durable value may legally contain '$'. ReplaceAllString would
// interpret it as a regexp expansion, so use a literal callback.
durable := item.value
value = item.wordBounded.ReplaceAllStringFunc(value, func(string) string { return durable })
}
return value
}