Replaced fmt, strings, sort, os, io, sync, encoding/json, path/filepath, errors, log, reflect with core.Sprintf, core.E, core.Contains, core.Trim, core.Split, core.Join, core.JoinPath, slices.Sort, c.Fs(), c.Lock(), core.JSONMarshal, core.ReadAll and other CoreGO v0.8.0 primitives. Framework boundary exceptions preserved where stdlib types are required by external interfaces (Gin, net/http, CGo, Wails, bubbletea). Co-Authored-By: Virgil <virgil@lethean.io>
248 lines
6.6 KiB
Go
248 lines
6.6 KiB
Go
// SPDX-License-Identifier: EUPL-1.2
|
|
|
|
package agentic
|
|
|
|
import (
|
|
"context"
|
|
"slices"
|
|
"time"
|
|
|
|
core "dappco.re/go/core"
|
|
)
|
|
|
|
// provider := agentic.NewProviderManager(nil).Provider("claude")
|
|
//
|
|
// core.Println(provider.Name()) // "claude"
|
|
type AgenticProviderInterface interface {
|
|
Generate(context.Context, string, map[string]any) (string, error)
|
|
Stream(context.Context, string, map[string]any, func(string)) error
|
|
Name() string
|
|
DefaultModel() string
|
|
IsAvailable() bool
|
|
}
|
|
|
|
// manager := agentic.NewProviderManager(nil)
|
|
// core.Println(manager.Names()) // ["claude", "gemini", "openai"]
|
|
type ProviderManager struct {
|
|
providers map[string]AgenticProviderInterface
|
|
}
|
|
|
|
var providerRetryBaseDelay = 100 * time.Millisecond
|
|
var providerSleep = time.Sleep
|
|
|
|
const providerRetryAttempts = 3
|
|
|
|
// manager := s.providerManager()
|
|
// core.Println(manager.Names()) // ["claude", "gemini", "openai"]
|
|
func (s *PrepSubsystem) providerManager() *ProviderManager {
|
|
if s == nil {
|
|
return NewProviderManager(nil)
|
|
}
|
|
if s.providers != nil {
|
|
return s.providers
|
|
}
|
|
|
|
s.providers = NewProviderManager(func(ctx context.Context, prompt string, options map[string]any) (string, error) {
|
|
config := anyMapValue(options["config"])
|
|
if model := contentMapStringValue(options, "model"); model != "" {
|
|
if config == nil {
|
|
config = map[string]any{}
|
|
}
|
|
config["model"] = model
|
|
}
|
|
input := ContentGenerateInput{
|
|
Prompt: prompt,
|
|
Provider: contentMapStringValue(options, "provider"),
|
|
Config: config,
|
|
}
|
|
if template := contentMapStringValue(options, "template"); template != "" {
|
|
input.Template = template
|
|
}
|
|
if briefID := contentMapStringValue(options, "brief_id", "briefId"); briefID != "" {
|
|
input.BriefID = briefID
|
|
}
|
|
result, err := s.contentGenerateResult(ctx, input)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return result.Content, nil
|
|
})
|
|
|
|
return s.providers
|
|
}
|
|
|
|
// manager := agentic.NewProviderManager(func(ctx context.Context, prompt string, options map[string]any) (string, error) {
|
|
// return "Draft ready", nil
|
|
// })
|
|
//
|
|
// core.Println(manager.Names()) // ["claude", "gemini", "openai"]
|
|
func NewProviderManager(generate ProviderGenerateFunc) *ProviderManager {
|
|
manager := &ProviderManager{
|
|
providers: make(map[string]AgenticProviderInterface),
|
|
}
|
|
|
|
manager.Register(newContentProvider("claude", "claude-3.7-sonnet", true, generate))
|
|
manager.Register(newContentProvider("gemini", "gemini-2.5-pro", true, generate))
|
|
manager.Register(newContentProvider("openai", "gpt-5.4", true, generate))
|
|
|
|
return manager
|
|
}
|
|
|
|
// provider, _ := manager.Provider("claude")
|
|
// text, _ := provider.Generate(ctx, "Draft a release note", map[string]any{"temperature": 0.2})
|
|
type ProviderGenerateFunc func(context.Context, string, map[string]any) (string, error)
|
|
|
|
// Stream sends provider output to the callback as it arrives.
|
|
//
|
|
// provider, _ := manager.Provider("claude")
|
|
// _ = provider.Stream(ctx, "Draft a release note", nil, func(token string) { core.Print(nil, token) })
|
|
type ProviderStreamFunc func(context.Context, string, map[string]any, func(string)) error
|
|
|
|
type contentProvider struct {
|
|
name string
|
|
defaultModel string
|
|
available bool
|
|
generate ProviderGenerateFunc
|
|
stream ProviderStreamFunc
|
|
}
|
|
|
|
func newContentProvider(name, defaultModel string, available bool, generate ProviderGenerateFunc) *contentProvider {
|
|
provider := &contentProvider{
|
|
name: name,
|
|
defaultModel: defaultModel,
|
|
available: available,
|
|
generate: generate,
|
|
}
|
|
provider.stream = func(ctx context.Context, prompt string, options map[string]any, onToken func(string)) error {
|
|
content, err := provider.Generate(ctx, prompt, options)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if onToken != nil {
|
|
onToken(content)
|
|
}
|
|
return nil
|
|
}
|
|
return provider
|
|
}
|
|
|
|
func (p *contentProvider) Generate(ctx context.Context, prompt string, options map[string]any) (string, error) {
|
|
if p.generate == nil {
|
|
return "", core.E("provider.generate", core.Concat("provider not configured: ", p.name), nil)
|
|
}
|
|
|
|
var lastErr error
|
|
delay := providerRetryBaseDelay
|
|
for attempt := 1; attempt <= providerRetryAttempts; attempt++ {
|
|
optionsCopy := map[string]any{}
|
|
for key, value := range options {
|
|
optionsCopy[key] = value
|
|
}
|
|
if optionsCopy["provider"] == nil {
|
|
optionsCopy["provider"] = p.name
|
|
}
|
|
if optionsCopy["model"] == nil && p.defaultModel != "" {
|
|
optionsCopy["model"] = p.defaultModel
|
|
}
|
|
|
|
content, err := p.generate(ctx, prompt, optionsCopy)
|
|
if err == nil {
|
|
return content, nil
|
|
}
|
|
lastErr = err
|
|
if attempt == providerRetryAttempts {
|
|
break
|
|
}
|
|
if ctx != nil {
|
|
select {
|
|
case <-ctx.Done():
|
|
return "", ctx.Err()
|
|
default:
|
|
}
|
|
}
|
|
if delay > 0 {
|
|
providerSleep(delay)
|
|
delay *= 2
|
|
continue
|
|
}
|
|
delay *= 2
|
|
}
|
|
|
|
return "", lastErr
|
|
}
|
|
|
|
func (p *contentProvider) Stream(ctx context.Context, prompt string, options map[string]any, onToken func(string)) error {
|
|
if p.stream == nil {
|
|
return core.E("provider.stream", core.Concat("provider not configured: ", p.name), nil)
|
|
}
|
|
return p.stream(ctx, prompt, options, onToken)
|
|
}
|
|
|
|
func (p *contentProvider) Name() string {
|
|
return p.name
|
|
}
|
|
|
|
func (p *contentProvider) DefaultModel() string {
|
|
return p.defaultModel
|
|
}
|
|
|
|
func (p *contentProvider) IsAvailable() bool {
|
|
return p.available
|
|
}
|
|
|
|
// Register adds or replaces a provider in the registry.
|
|
//
|
|
// manager.Register(newContentProvider("claude", "claude-3.7-sonnet", true, generate))
|
|
func (m *ProviderManager) Register(provider AgenticProviderInterface) {
|
|
if m == nil || provider == nil {
|
|
return
|
|
}
|
|
if m.providers == nil {
|
|
m.providers = make(map[string]AgenticProviderInterface)
|
|
}
|
|
m.providers[core.Lower(core.Trim(provider.Name()))] = provider
|
|
}
|
|
|
|
// Provider returns a registered provider by name.
|
|
//
|
|
// provider, ok := manager.Provider("openai")
|
|
func (m *ProviderManager) Provider(name string) (AgenticProviderInterface, bool) {
|
|
if m == nil {
|
|
return nil, false
|
|
}
|
|
provider, ok := m.providers[core.Lower(core.Trim(name))]
|
|
return provider, ok
|
|
}
|
|
|
|
// Names returns the registered provider names in deterministic order.
|
|
//
|
|
// core.Println(manager.Names()) // ["claude", "gemini", "openai"]
|
|
func (m *ProviderManager) Names() []string {
|
|
if m == nil || len(m.providers) == 0 {
|
|
return nil
|
|
}
|
|
|
|
names := make([]string, 0, len(m.providers))
|
|
for name := range m.providers {
|
|
names = append(names, name)
|
|
}
|
|
slices.Sort(names)
|
|
return names
|
|
}
|
|
|
|
// DefaultProvider returns the first registered provider that is available.
|
|
//
|
|
// provider := manager.DefaultProvider()
|
|
func (m *ProviderManager) DefaultProvider() AgenticProviderInterface {
|
|
if m == nil {
|
|
return nil
|
|
}
|
|
|
|
for _, name := range m.Names() {
|
|
if provider, ok := m.Provider(name); ok && provider.IsAvailable() {
|
|
return provider
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|