mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-06-18 00:47:44 +00:00
30708ae79d
* feat(auth): support named chatgpt oauth providers - add provider-scoped ChatGPT OAuth routes and CLI support - persist refresh tokens per provider and reject provider-type collisions - wire provider OAuth setup flows in the dashboard and setup UI Refs #448 * feat(agent): add chatgpt oauth account routing - add agent other_config routing for manual and round-robin selection - reuse routed provider resolution across resolver and pending loaders - add router, parser, and agent advanced dialog coverage for multi-account use Refs #448 * docs(api): describe chatgpt oauth routing - document named-provider ChatGPT OAuth auth routes - describe agent-side account routing and round-robin behavior - update OpenAPI agent config schema and provider type enum Refs #448 * fix(store): add missing agent key context helpers * feat(ui): clarify chatgpt oauth account setup and routing * docs(providers): align chatgpt oauth alias examples * feat(agent): add codex pool activity dashboard * fix(providers): harden codex oauth alias setup * feat(codex-pool): improve routing dashboard UX - redesign the Codex/OpenAI pool page around saved-pool checkpoints and live evidence - add clearer selection, attention, and recent-proof states for pool members - make the lower panels fill the remaining desktop viewport while staying responsive * fix(store): resolve context helper merge duplication * feat(oauth): add codex pool quota and observation APIs - add quota inspection and observation endpoints for ChatGPT Subscription (OAuth) providers - teach codex routing to surface pool activity, observation metadata, and quota-aware readiness - extend tests and HTTP docs/OpenAPI for the new pool monitoring flows * feat(web): add codex pool quota monitor and controls - add provider quota fetching, readiness badges, and live routing evidence on the account pool page - redesign pool setup and activity panels for multi-account management with localized copy updates - keep the live monitor internally scrollable and compact the account cards for better viewport fit * fix(web): clarify pool routing labels - rename the recent request badge from Direct to Selected - restore compact quota bars in the live pool cards * feat(codex-pool): add runtime health dashboard - derive per-provider success and failure health from routed Codex traces - surface routing, quota, and recent request evidence in the pool UI - align provider alias guidance and owner access with the dashboard role model * docs(auth): document tenant scoping and key roles * fix(auth): harden tenant and codex pool access control * fix(providers): align codex pool runtime defaults * feat(ui): tighten codex pool responsive layout * feat(chatgpt-oauth): refine codex pool management UX * feat(chatgpt-oauth): surface quota bars on provider pages - add compact quota bars to Codex provider rows and provider detail - fetch quota only for ready visible provider rows and ready detail aliases - fix managed-member detail visibility and tighten provider locale copy
155 lines
4.3 KiB
Go
155 lines
4.3 KiB
Go
package agent
|
|
|
|
import (
|
|
"regexp"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/providers"
|
|
)
|
|
|
|
// Regex patterns for extractive memory fallback.
|
|
var (
|
|
// Decisions: "decided to", "let's go with", "approved", "agreed on", "chose", "we'll use"
|
|
reDecision = regexp.MustCompile(`(?i)(?:decided\s+to|let'?s\s+go\s+with|approved|agreed\s+on|chose|we'?ll\s+use)\s+.{5,120}`)
|
|
|
|
// User preferences: "I prefer", "don't do", "always", "never", "I want", "please remember"
|
|
rePreference = regexp.MustCompile(`(?i)(?:I\s+prefer|don'?t\s+do|always\s+|never\s+|I\s+want|please\s+remember)\s+.{5,120}`)
|
|
|
|
// Technical facts: "the API is", "endpoint is", "version is", "uses X for Y"
|
|
reTechFact = regexp.MustCompile(`(?i)(?:the\s+API\s+is|endpoint\s+is|version\s+is|uses?\s+\S+\s+for)\s+.{3,120}`)
|
|
|
|
// URLs
|
|
reURL = regexp.MustCompile(`https?://[^\s)<>]{8,200}`)
|
|
|
|
// File paths (Unix-style and common project paths)
|
|
reFilePath = regexp.MustCompile(`(?:^|\s)([/~][\w./-]{5,120}|[\w-]+/[\w./-]{5,80})`)
|
|
|
|
// Dates in common formats
|
|
reDate = regexp.MustCompile(`\b\d{4}-\d{2}-\d{2}\b`)
|
|
)
|
|
|
|
// ExtractiveMemoryFallback extracts key information from conversation history
|
|
// using regex patterns. Used as a safety net when LLM-based memory flush
|
|
// returns NO_REPLY or produces no output, preventing context loss during compaction.
|
|
func ExtractiveMemoryFallback(history []providers.Message) string {
|
|
if len(history) == 0 {
|
|
return ""
|
|
}
|
|
|
|
// Collect only user and assistant content (skip system, tool)
|
|
var texts []string
|
|
for _, msg := range history {
|
|
if msg.Role == "user" || msg.Role == "assistant" {
|
|
if content := strings.TrimSpace(msg.Content); content != "" {
|
|
texts = append(texts, content)
|
|
}
|
|
}
|
|
}
|
|
if len(texts) == 0 {
|
|
return ""
|
|
}
|
|
|
|
combined := strings.Join(texts, "\n")
|
|
|
|
// Extract by category, dedup with a set
|
|
decisions := extractUnique(reDecision, combined)
|
|
preferences := extractUnique(rePreference, combined)
|
|
|
|
// Technical facts = regex matches + URLs + dates
|
|
techFacts := extractUnique(reTechFact, combined)
|
|
for _, u := range extractUnique(reURL, combined) {
|
|
techFacts = appendIfAbsent(techFacts, u)
|
|
}
|
|
for _, fp := range extractUniqueSubmatch(reFilePath, combined, 1) {
|
|
techFacts = appendIfAbsent(techFacts, fp)
|
|
}
|
|
for _, d := range extractUnique(reDate, combined) {
|
|
techFacts = appendIfAbsent(techFacts, d)
|
|
}
|
|
|
|
// Build output — only include non-empty sections
|
|
if len(decisions) == 0 && len(techFacts) == 0 && len(preferences) == 0 {
|
|
return ""
|
|
}
|
|
|
|
var sb strings.Builder
|
|
sb.WriteString("## Extracted Context (auto-saved before compaction)\n")
|
|
|
|
if len(decisions) > 0 {
|
|
sb.WriteString("\n### Decisions\n")
|
|
for _, d := range decisions {
|
|
sb.WriteString("- ")
|
|
sb.WriteString(strings.TrimSpace(d))
|
|
sb.WriteByte('\n')
|
|
}
|
|
}
|
|
|
|
if len(techFacts) > 0 {
|
|
sb.WriteString("\n### Key Facts\n")
|
|
for _, f := range techFacts {
|
|
sb.WriteString("- ")
|
|
sb.WriteString(strings.TrimSpace(f))
|
|
sb.WriteByte('\n')
|
|
}
|
|
}
|
|
|
|
if len(preferences) > 0 {
|
|
sb.WriteString("\n### User Preferences\n")
|
|
for _, p := range preferences {
|
|
sb.WriteString("- ")
|
|
sb.WriteString(strings.TrimSpace(p))
|
|
sb.WriteByte('\n')
|
|
}
|
|
}
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
// extractUnique returns deduplicated matches from a regex pattern.
|
|
func extractUnique(re *regexp.Regexp, text string) []string {
|
|
matches := re.FindAllString(text, -1)
|
|
return dedup(matches)
|
|
}
|
|
|
|
// extractUniqueSubmatch returns deduplicated submatch captures (by group index).
|
|
func extractUniqueSubmatch(re *regexp.Regexp, text string, group int) []string {
|
|
matches := re.FindAllStringSubmatch(text, -1)
|
|
var results []string
|
|
for _, m := range matches {
|
|
if group < len(m) {
|
|
s := strings.TrimSpace(m[group])
|
|
if s != "" {
|
|
results = append(results, s)
|
|
}
|
|
}
|
|
}
|
|
return dedup(results)
|
|
}
|
|
|
|
// dedup removes duplicate strings while preserving order.
|
|
func dedup(items []string) []string {
|
|
seen := make(map[string]struct{}, len(items))
|
|
var result []string
|
|
for _, item := range items {
|
|
trimmed := strings.TrimSpace(item)
|
|
if trimmed == "" {
|
|
continue
|
|
}
|
|
if _, ok := seen[trimmed]; ok {
|
|
continue
|
|
}
|
|
seen[trimmed] = struct{}{}
|
|
result = append(result, trimmed)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// appendIfAbsent appends s to slice only if not already present.
|
|
func appendIfAbsent(slice []string, s string) []string {
|
|
if slices.Contains(slice, s) {
|
|
return slice
|
|
}
|
|
return append(slice, s)
|
|
}
|