agent build
This commit is contained in:
157
go-api/internal/runtime/wire.go
Normal file
157
go-api/internal/runtime/wire.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"github.com/krow/krow-backend/go-api/internal/config"
|
||||
"github.com/krow/krow-backend/go-api/internal/gateway"
|
||||
"github.com/krow/krow-backend/go-api/internal/knowledge"
|
||||
"github.com/krow/krow-backend/go-api/internal/repo"
|
||||
"github.com/krow/krow-backend/go-api/internal/tools"
|
||||
)
|
||||
|
||||
// NewModelEngine builds the production runtime: the loader, the agent loop, a
|
||||
// live model gateway and a Postgres trajectory sink.
|
||||
//
|
||||
// One call, because the alternative is four, and four assembled at a call site
|
||||
// is how a deployment ends up running with a DiscardSink nobody chose. A test
|
||||
// that wants a fake model still reaches for NewEngine with WithAgentExecutor —
|
||||
// this function is the wiring, not a second way to configure the runtime.
|
||||
//
|
||||
// Skills keep the refusing stub. A skill has no executor of its own: the loop
|
||||
// runs agents, and a skill reaches a model only as a capability an agent
|
||||
// carries. Handing SkillExec a model would create a second, unbounded path to
|
||||
// one — which is exactly the shape I3 exists to prevent.
|
||||
func NewModelEngine(db repo.Querier, cfg config.Config) *Engine {
|
||||
gw := gateway.NewAnthropic(gateway.FromConfig(cfg.Model))
|
||||
retriever := knowledge.NewRetriever(db, NewEmbedder(cfg))
|
||||
exec := NewModelExecutor(gw, NewPostgresSink(db), DefaultTools(db, retriever)).
|
||||
WithRetriever(retriever)
|
||||
return NewEngine(db, WithAgentExecutor(exec))
|
||||
}
|
||||
|
||||
// NewEmbedder picks the embedding provider from configuration.
|
||||
//
|
||||
// Explicit first, then what is configured, then nothing. The order is the whole
|
||||
// design: three providers all return vectors and retrieval works with any of
|
||||
// them, so a deployment running the wrong one looks identical to one running
|
||||
// the right one until somebody phrases a question differently. Naming the
|
||||
// provider is how that stops being a silent condition.
|
||||
//
|
||||
// ollama A model on this machine. Real semantics, no credential, no
|
||||
// per-token cost, no tenant text leaving the host. The default
|
||||
// worth reaching for.
|
||||
// voyage Hosted. Better on subtle retrieval over a large messy corpus,
|
||||
// and the only one that needs a credential.
|
||||
// lexical The deterministic stand-in. NOT semantic — it matches shared
|
||||
// vocabulary and nothing else. Development only; config.validate
|
||||
// refuses it in production.
|
||||
//
|
||||
// Returns nil when nothing is configured, and retrieval then runs keyword-only,
|
||||
// saying so on every result. Nil rather than a hosted client with an empty key:
|
||||
// both end up keyword-only, but nil says "no embedder is configured" once, at
|
||||
// wiring time, instead of failing an HTTP call per query to learn the same
|
||||
// thing.
|
||||
func NewEmbedder(cfg config.Config) knowledge.Embedder {
|
||||
k := cfg.Knowledge
|
||||
|
||||
provider := k.EmbedProvider
|
||||
if provider == "" {
|
||||
// Nothing named. Infer from what is actually present, preferring the
|
||||
// one that costs nothing and keeps text local.
|
||||
switch {
|
||||
case k.UseLexicalEmbedder:
|
||||
provider = "lexical"
|
||||
case k.EmbedBaseURL != "":
|
||||
provider = "ollama"
|
||||
case k.EmbedAPIKey != "":
|
||||
provider = "voyage"
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
switch provider {
|
||||
case "ollama":
|
||||
return knowledge.NewOllama(k.EmbedBaseURL, k.EmbedModel, k.EmbedDims)
|
||||
|
||||
case "voyage":
|
||||
if k.EmbedAPIKey == "" {
|
||||
// Named but unusable. Nil, so retrieval degrades honestly rather
|
||||
// than failing a request per query on a credential nobody set.
|
||||
return nil
|
||||
}
|
||||
model, dims := k.EmbedModel, k.EmbedDims
|
||||
if model == "" {
|
||||
model = knowledge.DefaultVoyageModel
|
||||
}
|
||||
if dims == 0 {
|
||||
dims = knowledge.DefaultVoyageDims
|
||||
}
|
||||
return knowledge.NewVoyage(k.EmbedAPIKey, model, dims)
|
||||
|
||||
case "lexical":
|
||||
dims := k.EmbedDims
|
||||
if dims == 0 {
|
||||
dims = 256
|
||||
}
|
||||
e := knowledge.NewLexical(dims)
|
||||
// Told what environment it is in, so its own refusal is the backstop
|
||||
// behind config.validate's.
|
||||
e.Production = cfg.AppEnv == "production"
|
||||
return e
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DefaultTools is the tool registry this service ships with.
|
||||
//
|
||||
// One function, so "which tools exist" has a single answer that a test and the
|
||||
// server reach the same way. Registration panics on a malformed tool: a
|
||||
// service that booted without a capability its specs name would fail one run
|
||||
// at a time instead of once, loudly, at startup.
|
||||
func DefaultTools(db repo.Querier, retriever *knowledge.Retriever) *tools.Registry {
|
||||
// The confirmation store is Postgres-backed, not in-process. A pending
|
||||
// write is asked about in one request and approved in another, and nothing
|
||||
// guarantees those two reach the same replica — an in-memory store would
|
||||
// refuse a large share of perfectly good approvals, for a reason invisible
|
||||
// to the person clicking. See tools.MemoryStore's own warning.
|
||||
reg := tools.NewRegistryWithStore(tools.NewPostgresStore(db))
|
||||
for _, t := range []tools.Tool{
|
||||
// Activity
|
||||
tools.ActivityBreakdown(db),
|
||||
tools.ActivitySignals(db),
|
||||
// Workforce
|
||||
tools.WorkforceAttendance(db),
|
||||
tools.WorkforceOvertime(db),
|
||||
tools.WorkforceCoverage(db),
|
||||
tools.WorkforceTraining(db),
|
||||
// Hiring
|
||||
tools.CandidatesQuality(db),
|
||||
tools.HiresRecent(db),
|
||||
tools.HiresPerformance(db),
|
||||
tools.PositionsRisk(db),
|
||||
tools.TalentPool(db),
|
||||
// Cross-domain
|
||||
tools.WorkspaceSummary(db),
|
||||
tools.OperationsRisk(db),
|
||||
// Assignments: the two lookups that yield ids, and the one write that
|
||||
// consumes them. assign_worker is the only tool here with an effect,
|
||||
// and it cannot run without an approval — see tools/confirm.go.
|
||||
tools.OpenPositions(db),
|
||||
tools.AvailableWorkers(db),
|
||||
tools.AssignWorker(db),
|
||||
// The hiring funnel: the lookup that yields application ids, and the
|
||||
// write that moves somebody through it. Replaces the browser panel's
|
||||
// interview matcher, which was the one capability the old templates had
|
||||
// that the tool layer did not.
|
||||
tools.CandidatesAwaiting(db),
|
||||
tools.MoveApplication(db),
|
||||
// Knowledge. Registered once; which corpora it may read comes from the
|
||||
// running agent's spec by way of the tool Context, so this single
|
||||
// registration serves every agent without any of them being able to
|
||||
// name another's documents.
|
||||
tools.KnowledgeSearch(retriever),
|
||||
} {
|
||||
reg.MustRegister(t)
|
||||
}
|
||||
return reg
|
||||
}
|
||||
Reference in New Issue
Block a user