256 lines
6.4 KiB
Go
256 lines
6.4 KiB
Go
package action
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"fmt"
|
||
|
|
"os"
|
||
|
|
|
||
|
|
"github.com/rockliang/poimen/workflows/internal/memory"
|
||
|
|
)
|
||
|
|
|
||
|
|
// RetrieveMemoryInput input for RetrieveMemoryActivity
|
||
|
|
type RetrieveMemoryInput struct {
|
||
|
|
// Query semantic search query
|
||
|
|
Query string `json:"query"`
|
||
|
|
|
||
|
|
// Project memory project (default: "poimen")
|
||
|
|
Project string `json:"project,omitempty"`
|
||
|
|
|
||
|
|
// Scope retrieval scope: "skills", "lessons", "all" (default: "all")
|
||
|
|
Scope string `json:"scope,omitempty"`
|
||
|
|
|
||
|
|
// Limit max results (default: 10)
|
||
|
|
Limit int `json:"limit,omitempty"`
|
||
|
|
|
||
|
|
// LevelFilter filter by level: L1, L2, R (reference)
|
||
|
|
LevelFilter []string `json:"levelFilter,omitempty"`
|
||
|
|
|
||
|
|
// Tool tool context for skill matching
|
||
|
|
Tool string `json:"tool,omitempty"`
|
||
|
|
|
||
|
|
// Task task description for context retrieval
|
||
|
|
Task string `json:"task,omitempty"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// RetrieveMemoryOutput output from RetrieveMemoryActivity
|
||
|
|
type RetrieveMemoryOutput struct {
|
||
|
|
// Skills relevant skills found
|
||
|
|
Skills []MemorySkill `json:"skills"`
|
||
|
|
|
||
|
|
// Lessons relevant lessons/knowledge found
|
||
|
|
Lessons []MemoryLesson `json:"lessons"`
|
||
|
|
|
||
|
|
// References reference documents found
|
||
|
|
References []MemoryReference `json:"references"`
|
||
|
|
|
||
|
|
// TotalResults total results found
|
||
|
|
TotalResults int `json:"totalResults"`
|
||
|
|
|
||
|
|
// Budget token budget info
|
||
|
|
Budget MemoryBudget `json:"budget"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// MemorySkill skill from memory
|
||
|
|
type MemorySkill struct {
|
||
|
|
Name string `json:"name"`
|
||
|
|
Description string `json:"description"`
|
||
|
|
Why string `json:"why,omitempty"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// MemoryLesson lesson from memory
|
||
|
|
type MemoryLesson struct {
|
||
|
|
ID string `json:"id"`
|
||
|
|
Text string `json:"text"`
|
||
|
|
Level string `json:"level"`
|
||
|
|
Score float32 `json:"score"`
|
||
|
|
Breadcrumb string `json:"breadcrumb,omitempty"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// MemoryReference reference document from memory
|
||
|
|
type MemoryReference struct {
|
||
|
|
ID string `json:"id"`
|
||
|
|
Text string `json:"text"`
|
||
|
|
Score float32 `json:"score"`
|
||
|
|
Breadcrumb string `json:"breadcrumb,omitempty"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// MemoryBudget token budget tracking
|
||
|
|
type MemoryBudget struct {
|
||
|
|
Requested int `json:"requested"`
|
||
|
|
Used int `json:"used"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// RetrieveMemoryActivity retrieves relevant knowledge from poimen-memory
|
||
|
|
func RetrieveMemoryActivity(ctx context.Context, in RetrieveMemoryInput) (RetrieveMemoryOutput, error) {
|
||
|
|
logger := newActivityLogger(ctx)
|
||
|
|
logger.Info("RetrieveMemoryActivity started", "query", in.Query, "scope", in.Scope)
|
||
|
|
|
||
|
|
output := RetrieveMemoryOutput{
|
||
|
|
Skills: []MemorySkill{},
|
||
|
|
Lessons: []MemoryLesson{},
|
||
|
|
References: []MemoryReference{},
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get memory service URL and token
|
||
|
|
baseURL := os.Getenv("POIMEN_MEMORY_URL")
|
||
|
|
if baseURL == "" {
|
||
|
|
baseURL = "http://poimen-memory.poimen.svc.cluster.local:8080"
|
||
|
|
}
|
||
|
|
|
||
|
|
token := os.Getenv("POIMEN_MEMORY_TOKEN")
|
||
|
|
// Token optional for internal cluster access
|
||
|
|
|
||
|
|
// Set defaults
|
||
|
|
project := in.Project
|
||
|
|
if project == "" {
|
||
|
|
project = "poimen"
|
||
|
|
}
|
||
|
|
|
||
|
|
scope := in.Scope
|
||
|
|
if scope == "" {
|
||
|
|
scope = "all"
|
||
|
|
}
|
||
|
|
|
||
|
|
limit := in.Limit
|
||
|
|
if limit == 0 {
|
||
|
|
limit = 10
|
||
|
|
}
|
||
|
|
|
||
|
|
client := memory.NewClient(baseURL, token)
|
||
|
|
|
||
|
|
// If tool/task provided, use Context API for skill matching
|
||
|
|
if in.Tool != "" || in.Task != "" {
|
||
|
|
contextResp, err := client.Context(ctx, &memory.ContextRequest{
|
||
|
|
Project: project,
|
||
|
|
Tool: in.Tool,
|
||
|
|
Task: in.Task,
|
||
|
|
SignatureSource: in.Query,
|
||
|
|
Scope: "tool_context",
|
||
|
|
Budget: 8192,
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
logger.Warn("Context retrieval failed, falling back to query", "error", err)
|
||
|
|
} else {
|
||
|
|
// Extract skills
|
||
|
|
for _, skill := range contextResp.Skills {
|
||
|
|
output.Skills = append(output.Skills, MemorySkill{
|
||
|
|
Name: skill.Name,
|
||
|
|
Why: skill.Why,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
// Extract lessons
|
||
|
|
for i, lesson := range contextResp.Lessons {
|
||
|
|
output.Lessons = append(output.Lessons, MemoryLesson{
|
||
|
|
ID: fmt.Sprintf("ctx-%d", i),
|
||
|
|
Text: lesson.Text,
|
||
|
|
Level: lesson.Level,
|
||
|
|
Score: lesson.Score,
|
||
|
|
Breadcrumb: "",
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
output.Budget = MemoryBudget{
|
||
|
|
Requested: contextResp.Budget.Requested,
|
||
|
|
Used: contextResp.Budget.Used,
|
||
|
|
}
|
||
|
|
output.TotalResults = len(output.Skills) + len(output.Lessons)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Also do semantic query for additional context
|
||
|
|
if scope == "all" || scope == "lessons" || scope == "references" {
|
||
|
|
levelFilter := in.LevelFilter
|
||
|
|
if len(levelFilter) == 0 {
|
||
|
|
levelFilter = []string{"L1", "L2"}
|
||
|
|
}
|
||
|
|
|
||
|
|
queryResp, err := client.Query(ctx, &memory.QueryRequest{
|
||
|
|
Project: project,
|
||
|
|
Query: in.Query,
|
||
|
|
LevelFilter: levelFilter,
|
||
|
|
Limit: limit,
|
||
|
|
Scope: "all",
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
logger.Warn("Query failed", "error", err)
|
||
|
|
} else {
|
||
|
|
for _, result := range queryResp.Results {
|
||
|
|
if result.Level == "R" {
|
||
|
|
output.References = append(output.References, MemoryReference{
|
||
|
|
ID: result.ID,
|
||
|
|
Text: result.Text,
|
||
|
|
Score: result.Score,
|
||
|
|
Breadcrumb: result.Breadcrumb,
|
||
|
|
})
|
||
|
|
} else {
|
||
|
|
// Avoid duplicates from Context call
|
||
|
|
found := false
|
||
|
|
for _, existing := range output.Lessons {
|
||
|
|
if existing.ID == result.ID {
|
||
|
|
found = true
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if !found {
|
||
|
|
output.Lessons = append(output.Lessons, MemoryLesson{
|
||
|
|
ID: result.ID,
|
||
|
|
Text: result.Text,
|
||
|
|
Level: result.Level,
|
||
|
|
Score: result.Score,
|
||
|
|
Breadcrumb: result.Breadcrumb,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
output.TotalResults = len(output.Skills) + len(output.Lessons) + len(output.References)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
logger.Info("RetrieveMemoryActivity completed",
|
||
|
|
"skills", len(output.Skills),
|
||
|
|
"lessons", len(output.Lessons),
|
||
|
|
"references", len(output.References))
|
||
|
|
|
||
|
|
return output, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// FormatMemoryForPrompt formats memory output for LLM prompt injection
|
||
|
|
func FormatMemoryForPrompt(mem RetrieveMemoryOutput) string {
|
||
|
|
if mem.TotalResults == 0 {
|
||
|
|
return ""
|
||
|
|
}
|
||
|
|
|
||
|
|
var result string
|
||
|
|
|
||
|
|
if len(mem.Skills) > 0 {
|
||
|
|
result += "\n## Relevant Skills\n"
|
||
|
|
for _, skill := range mem.Skills {
|
||
|
|
result += fmt.Sprintf("- **%s**: %s\n", skill.Name, skill.Why)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if len(mem.Lessons) > 0 {
|
||
|
|
result += "\n## Relevant Knowledge\n"
|
||
|
|
for _, lesson := range mem.Lessons {
|
||
|
|
result += fmt.Sprintf("- [%s] %s\n", lesson.Level, truncate(lesson.Text, 200))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if len(mem.References) > 0 {
|
||
|
|
result += "\n## Reference Documents\n"
|
||
|
|
for _, ref := range mem.References {
|
||
|
|
result += fmt.Sprintf("- %s\n", truncate(ref.Text, 200))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return result
|
||
|
|
}
|
||
|
|
|
||
|
|
func truncate(s string, maxLen int) string {
|
||
|
|
if len(s) <= maxLen {
|
||
|
|
return s
|
||
|
|
}
|
||
|
|
return s[:maxLen] + "..."
|
||
|
|
}
|