Merge branch 'refactor/praxis-capability-registry' into integration/small-batch
This commit is contained in:
+165
-109
@@ -12,6 +12,68 @@ import (
|
||||
"github.com/kami/maven/internal/router"
|
||||
)
|
||||
|
||||
// praxisCapability is one arm of the Praxis act dispatch. This is an interface
|
||||
// rather than a map[string]func because each arm carries its own state: the
|
||||
// verb aliases it answers to, the trace name it records, and its own reply
|
||||
// formatting. The dispatch grows an arm per Praxis capability, so a new one is
|
||||
// added to praxisCapabilities below and nothing else changes.
|
||||
type praxisCapability interface {
|
||||
// aliases are the verbs (router fn slots, EN and RU) this capability answers to.
|
||||
aliases() []string
|
||||
// handle runs the capability and returns the user-facing reply.
|
||||
handle(ctx context.Context, h *reactiveHandler, px *praxisClient, dec router.Decision) string
|
||||
}
|
||||
|
||||
// praxisCapabilities is the registry handlePraxisAct consults, in order.
|
||||
var praxisCapabilities = []praxisCapability{
|
||||
listAttentionCapability{},
|
||||
praxisItemAction{
|
||||
verbs: []string{"acknowledge_item", "принято", "понял", "поняла"},
|
||||
ask: "какой пункт отметить принятым?",
|
||||
op: "acknowledge",
|
||||
failure: "не получилось отметить принятым.",
|
||||
success: "принято.",
|
||||
call: func(ctx context.Context, px *praxisClient, id string) error {
|
||||
_, err := px.Acknowledge(ctx, id)
|
||||
return err
|
||||
},
|
||||
},
|
||||
praxisItemAction{
|
||||
verbs: []string{"resolve_item", "сделано", "готово", "решено"},
|
||||
ask: "какой пункт отметить сделанным?",
|
||||
op: "resolve",
|
||||
failure: "не получилось отметить сделанным.",
|
||||
success: "отмечено как сделано.",
|
||||
call: func(ctx context.Context, px *praxisClient, id string) error {
|
||||
_, err := px.Resolve(ctx, id)
|
||||
return err
|
||||
},
|
||||
},
|
||||
praxisItemAction{
|
||||
verbs: []string{"ignore_item", "игнорировать", "неважно"},
|
||||
ask: "какой пункт игнорировать?",
|
||||
op: "ignore",
|
||||
failure: "не получилось проигнорировать.",
|
||||
success: "проигнорировано.",
|
||||
call: func(ctx context.Context, px *praxisClient, id string) error {
|
||||
_, err := px.Ignore(ctx, id)
|
||||
return err
|
||||
},
|
||||
},
|
||||
praxisItemAction{
|
||||
verbs: []string{"pin_item", "закрепить"},
|
||||
ask: "какой пункт закрепить?",
|
||||
op: "pin",
|
||||
failure: "не получилось закрепить.",
|
||||
success: "закреплено.",
|
||||
call: func(ctx context.Context, px *praxisClient, id string) error {
|
||||
_, err := px.Pin(ctx, id, true)
|
||||
return err
|
||||
},
|
||||
},
|
||||
listChangesCapability{},
|
||||
}
|
||||
|
||||
// handlePraxisAct — dispatches ecosystem tool acts through the Praxis tools API.
|
||||
// Returns "" when the act is not a Praxis verb (the caller falls through to the
|
||||
// system command executor). Returns a reply string otherwise.
|
||||
@@ -20,118 +82,112 @@ func (h *reactiveHandler) handlePraxisAct(ctx context.Context, dec router.Decisi
|
||||
return ""
|
||||
}
|
||||
px := h.ecosystem.praxis
|
||||
fn := dec.Slots.Fn
|
||||
|
||||
// Map verbs and Russian aliases to Praxis tool calls.
|
||||
// Each case: if the verb matches, call the tool and return a user-facing reply.
|
||||
switch fn {
|
||||
case "list_attention", "attention", "внимание", "что требует внимания", "что нового":
|
||||
items, err := px.ListAttention(ctx, 20)
|
||||
if err != nil {
|
||||
log.Printf("ecosystem: praxis attention: %v", err)
|
||||
return "не могу сейчас узнать, что требует внимания."
|
||||
}
|
||||
if len(items) == 0 {
|
||||
return "ничего не требует внимания."
|
||||
}
|
||||
h.recordPraxisTrace(ctx, "list_attention", map[string]any{"count": len(items)})
|
||||
var parts []string
|
||||
for _, item := range items {
|
||||
title, _ := item["title"].(string)
|
||||
// importance arrives as JSON number ⇒ float64 over the HTTP contract.
|
||||
importance, _ := item["importance"].(float64)
|
||||
rule, _ := item["rule"].(string)
|
||||
s := title
|
||||
if importance > 0 {
|
||||
s += fmt.Sprintf(" (важность %d", int(importance))
|
||||
if rule != "" {
|
||||
s += ": " + rule
|
||||
}
|
||||
s += ")"
|
||||
}
|
||||
parts = append(parts, s)
|
||||
|
||||
// Speaking an item surfaces it, it does not acknowledge it
|
||||
// (ECOSYSTEM-SPEC.md §2.3: surfaced != acknowledged). Best-effort:
|
||||
// a failed surface call must not block delivering the digest.
|
||||
if id, ok := item["id"].(string); ok && id != "" {
|
||||
if _, err := px.Surface(ctx, id); err != nil {
|
||||
log.Printf("ecosystem: praxis surface %s: %v", id, err)
|
||||
}
|
||||
for _, capability := range praxisCapabilities {
|
||||
for _, alias := range capability.aliases() {
|
||||
if alias == dec.Slots.Fn {
|
||||
return capability.handle(ctx, h, px, dec)
|
||||
}
|
||||
}
|
||||
return "требует внимания: " + strings.Join(parts, "; ")
|
||||
|
||||
case "acknowledge_item", "принято", "понял", "поняла":
|
||||
id := dec.Slots.Value
|
||||
if id == "" {
|
||||
return "какой пункт отметить принятым?"
|
||||
}
|
||||
if _, err := px.Acknowledge(ctx, id); err != nil {
|
||||
log.Printf("ecosystem: praxis acknowledge %s: %v", id, err)
|
||||
return "не получилось отметить принятым."
|
||||
}
|
||||
h.recordPraxisTrace(ctx, "acknowledge", map[string]any{"item_id": id})
|
||||
return "принято."
|
||||
|
||||
case "resolve_item", "сделано", "готово", "решено":
|
||||
id := dec.Slots.Value
|
||||
if id == "" {
|
||||
return "какой пункт отметить сделанным?"
|
||||
}
|
||||
if _, err := px.Resolve(ctx, id); err != nil {
|
||||
log.Printf("ecosystem: praxis resolve %s: %v", id, err)
|
||||
return "не получилось отметить сделанным."
|
||||
}
|
||||
h.recordPraxisTrace(ctx, "resolve", map[string]any{"item_id": id})
|
||||
return "отмечено как сделано."
|
||||
|
||||
case "ignore_item", "игнорировать", "неважно":
|
||||
id := dec.Slots.Value
|
||||
if id == "" {
|
||||
return "какой пункт игнорировать?"
|
||||
}
|
||||
if _, err := px.Ignore(ctx, id); err != nil {
|
||||
log.Printf("ecosystem: praxis ignore %s: %v", id, err)
|
||||
return "не получилось проигнорировать."
|
||||
}
|
||||
h.recordPraxisTrace(ctx, "ignore", map[string]any{"item_id": id})
|
||||
return "проигнорировано."
|
||||
|
||||
case "pin_item", "закрепить":
|
||||
id := dec.Slots.Value
|
||||
if id == "" {
|
||||
return "какой пункт закрепить?"
|
||||
}
|
||||
if _, err := px.Pin(ctx, id, true); err != nil {
|
||||
log.Printf("ecosystem: praxis pin %s: %v", id, err)
|
||||
return "не получилось закрепить."
|
||||
}
|
||||
h.recordPraxisTrace(ctx, "pin", map[string]any{"item_id": id})
|
||||
return "закреплено."
|
||||
|
||||
case "list_changes", "changes", "изменения", "что изменилось":
|
||||
changes, err := px.ListChanges(ctx, 20)
|
||||
if err != nil {
|
||||
log.Printf("ecosystem: praxis changes: %v", err)
|
||||
return "не могу сейчас узнать об изменениях."
|
||||
}
|
||||
if len(changes) == 0 {
|
||||
return "нет изменений."
|
||||
}
|
||||
h.recordPraxisTrace(ctx, "list_changes", map[string]any{"count": len(changes)})
|
||||
var parts []string
|
||||
for _, c := range changes {
|
||||
title, _ := c["title"].(string)
|
||||
typ, _ := c["change_type"].(string)
|
||||
parts = append(parts, fmt.Sprintf("%s (%s)", title, typ))
|
||||
}
|
||||
return "изменения: " + strings.Join(parts, "; ")
|
||||
|
||||
default:
|
||||
// Not a Praxis verb — let the caller fall through.
|
||||
return ""
|
||||
}
|
||||
// Not a Praxis verb — let the caller fall through.
|
||||
return ""
|
||||
}
|
||||
|
||||
// praxisItemAction is the shared shape of the item-lifecycle capabilities: take
|
||||
// an item id from the value slot, call one Praxis endpoint, trace the result.
|
||||
type praxisItemAction struct {
|
||||
verbs []string
|
||||
ask string // reply when no item id was given
|
||||
op string // trace + log name of the operation
|
||||
failure string // reply when the Praxis call errors
|
||||
success string
|
||||
call func(ctx context.Context, px *praxisClient, id string) error
|
||||
}
|
||||
|
||||
func (a praxisItemAction) aliases() []string { return a.verbs }
|
||||
|
||||
func (a praxisItemAction) handle(ctx context.Context, h *reactiveHandler, px *praxisClient, dec router.Decision) string {
|
||||
id := dec.Slots.Value
|
||||
if id == "" {
|
||||
return a.ask
|
||||
}
|
||||
if err := a.call(ctx, px, id); err != nil {
|
||||
log.Printf("ecosystem: praxis %s %s: %v", a.op, id, err)
|
||||
return a.failure
|
||||
}
|
||||
h.recordPraxisTrace(ctx, a.op, map[string]any{"item_id": id})
|
||||
return a.success
|
||||
}
|
||||
|
||||
// listAttentionCapability reads the attention digest and surfaces every item it speaks.
|
||||
type listAttentionCapability struct{}
|
||||
|
||||
func (listAttentionCapability) aliases() []string {
|
||||
return []string{"list_attention", "attention", "внимание", "что требует внимания", "что нового"}
|
||||
}
|
||||
|
||||
func (listAttentionCapability) handle(ctx context.Context, h *reactiveHandler, px *praxisClient, _ router.Decision) string {
|
||||
items, err := px.ListAttention(ctx, 20)
|
||||
if err != nil {
|
||||
log.Printf("ecosystem: praxis attention: %v", err)
|
||||
return "не могу сейчас узнать, что требует внимания."
|
||||
}
|
||||
if len(items) == 0 {
|
||||
return "ничего не требует внимания."
|
||||
}
|
||||
h.recordPraxisTrace(ctx, "list_attention", map[string]any{"count": len(items)})
|
||||
var parts []string
|
||||
for _, item := range items {
|
||||
title, _ := item["title"].(string)
|
||||
// importance arrives as JSON number ⇒ float64 over the HTTP contract.
|
||||
importance, _ := item["importance"].(float64)
|
||||
rule, _ := item["rule"].(string)
|
||||
s := title
|
||||
if importance > 0 {
|
||||
s += fmt.Sprintf(" (важность %d", int(importance))
|
||||
if rule != "" {
|
||||
s += ": " + rule
|
||||
}
|
||||
s += ")"
|
||||
}
|
||||
parts = append(parts, s)
|
||||
|
||||
// Speaking an item surfaces it, it does not acknowledge it
|
||||
// (ECOSYSTEM-SPEC.md §2.3: surfaced != acknowledged). Best-effort:
|
||||
// a failed surface call must not block delivering the digest.
|
||||
if id, ok := item["id"].(string); ok && id != "" {
|
||||
if _, err := px.Surface(ctx, id); err != nil {
|
||||
log.Printf("ecosystem: praxis surface %s: %v", id, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return "требует внимания: " + strings.Join(parts, "; ")
|
||||
}
|
||||
|
||||
// listChangesCapability reads the recent-changes feed.
|
||||
type listChangesCapability struct{}
|
||||
|
||||
func (listChangesCapability) aliases() []string {
|
||||
return []string{"list_changes", "changes", "изменения", "что изменилось"}
|
||||
}
|
||||
|
||||
func (listChangesCapability) handle(ctx context.Context, h *reactiveHandler, px *praxisClient, _ router.Decision) string {
|
||||
changes, err := px.ListChanges(ctx, 20)
|
||||
if err != nil {
|
||||
log.Printf("ecosystem: praxis changes: %v", err)
|
||||
return "не могу сейчас узнать об изменениях."
|
||||
}
|
||||
if len(changes) == 0 {
|
||||
return "нет изменений."
|
||||
}
|
||||
h.recordPraxisTrace(ctx, "list_changes", map[string]any{"count": len(changes)})
|
||||
var parts []string
|
||||
for _, c := range changes {
|
||||
title, _ := c["title"].(string)
|
||||
typ, _ := c["change_type"].(string)
|
||||
parts = append(parts, fmt.Sprintf("%s (%s)", title, typ))
|
||||
}
|
||||
return "изменения: " + strings.Join(parts, "; ")
|
||||
}
|
||||
|
||||
// recordPraxisTrace — writes a fact recording a cross-service ecosystem call.
|
||||
|
||||
Reference in New Issue
Block a user