248 lines
6.6 KiB
Go
248 lines
6.6 KiB
Go
// SPDX-License-Identifier: EUPL-1.2
|
|
|
|
package agentic
|
|
|
|
import (
|
|
"context"
|
|
"sort"
|
|
"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)
|
|
}
|
|
sort.Strings(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
|
|
}
|