agent
This commit is contained in:
@@ -53,42 +53,6 @@ type Agent struct {
|
||||
MaxToolCalls int
|
||||
}
|
||||
|
||||
// DefaultAgents are the agents phase 2 ships with.
|
||||
//
|
||||
// One, deliberately. The loop, the gateway, the transport and the audit trail
|
||||
// are the parts that are expensive to change later, and they are easier to get
|
||||
// right against a single agent with a single tool than across five. The other
|
||||
// four are a config change once this one is proven — which is phase 3, where
|
||||
// they move to YAML and stop being Go at all.
|
||||
var DefaultAgents = map[string]Agent{
|
||||
"orders": {
|
||||
Name: "orders",
|
||||
Tier: utils.TierBalanced,
|
||||
System: strings.TrimSpace(`
|
||||
You are Nearle Buddy, helping a shopkeeper run their business from the Nearle console.
|
||||
|
||||
Answer from tool results and nothing else. Every number you state must have come
|
||||
from a tool in this conversation. If no tool can answer, say so plainly and say
|
||||
what you would need — never estimate, and never fill a gap from general knowledge
|
||||
about retail.
|
||||
|
||||
When a tool returns no rows, that is an answer: say there are none. Do not
|
||||
describe an empty result as a problem with the system.
|
||||
|
||||
When a result says it was truncated, say so in your reply. Do not describe a
|
||||
capped list as the full picture.
|
||||
|
||||
Say what your answer covers — one branch or all of them — using the scope the
|
||||
tool reports.
|
||||
|
||||
Be brief. A shopkeeper reading this is mid-shift: lead with the answer, then the
|
||||
detail that makes it actionable. No preamble, no restating the question.`),
|
||||
Tools: []string{"stuck_orders"},
|
||||
MaxSteps: 4,
|
||||
MaxToolCalls: 6,
|
||||
},
|
||||
}
|
||||
|
||||
// AssistantAnswer is what one question produced.
|
||||
type AssistantAnswer struct {
|
||||
Reply string `json:"reply"`
|
||||
@@ -103,6 +67,12 @@ type AssistantAnswer struct {
|
||||
// model finished. The reply is still returned — a partial answer beats a
|
||||
// spinner — but it is flagged rather than passed off as complete.
|
||||
Incomplete bool `json:"incomplete,omitempty"`
|
||||
// Set when the assistant resolved a change and is waiting on the person.
|
||||
//
|
||||
// One card, never a list. A card proposing several actions hides the one
|
||||
// they would have refused, so the loop returns the first and stops — the
|
||||
// next change is asked for separately.
|
||||
Awaiting *tools.Proposal `json:"awaiting,omitempty"`
|
||||
}
|
||||
|
||||
// AssistantStep is one tool call, for the console to render.
|
||||
@@ -119,6 +89,12 @@ type AssistantStep struct {
|
||||
// AssistantService answers a question.
|
||||
type AssistantService interface {
|
||||
Ask(ctx context.Context, agentName, question string, caller tools.Caller) (AssistantAnswer, error)
|
||||
// Approve performs a change the person has agreed to.
|
||||
//
|
||||
// Takes no question and involves no model: the card names the action, and
|
||||
// the registry re-validates it against the live database. The assistant is
|
||||
// not in this call at all, which is the point of splitting it out.
|
||||
Approve(ctx context.Context, agentName, card string, caller tools.Caller) (AssistantAnswer, error)
|
||||
// Available reports whether typed questions work at all here.
|
||||
Available() bool
|
||||
}
|
||||
@@ -129,15 +105,42 @@ type assistantService struct {
|
||||
agents map[string]Agent
|
||||
}
|
||||
|
||||
// NewAssistantService takes its agents already loaded and validated.
|
||||
//
|
||||
// No fallback to a built-in set: a deployment whose agent files failed to load
|
||||
// should refuse to start, not quietly run a different assistant than the one
|
||||
// its configuration describes.
|
||||
func NewAssistantService(registry *tools.Registry, chat utils.Chat, agents map[string]Agent) AssistantService {
|
||||
if agents == nil {
|
||||
agents = DefaultAgents
|
||||
}
|
||||
return &assistantService{registry: registry, chat: chat, agents: agents}
|
||||
}
|
||||
|
||||
func (s *assistantService) Available() bool { return s.chat != nil }
|
||||
|
||||
func (s *assistantService) Approve(ctx context.Context, agentName, card string, caller tools.Caller) (AssistantAnswer, error) {
|
||||
agent, known := s.agents[agentName]
|
||||
if !known {
|
||||
return AssistantAnswer{}, fmt.Errorf("no assistant called %q", agentName)
|
||||
}
|
||||
|
||||
// No model is consulted. A deployment with no provider can still approve a
|
||||
// card it issued earlier, which matters: the change is the person's
|
||||
// decision, and it should not stop being possible because a provider is
|
||||
// down.
|
||||
result, err := s.registry.Approve(ctx, tools.Agent{Name: agent.Name, Tools: agent.Tools}, card, caller)
|
||||
if err != nil {
|
||||
return AssistantAnswer{}, err
|
||||
}
|
||||
|
||||
answer := AssistantAnswer{
|
||||
Reply: result.Note,
|
||||
Used: []AssistantStep{{Tool: "approved", Outcome: tools.OutcomeOK, Rows: result.Count, Scope: result.Scope}},
|
||||
}
|
||||
if result.Source != "" {
|
||||
answer.Sources = []string{result.Source}
|
||||
}
|
||||
return answer, nil
|
||||
}
|
||||
|
||||
// maxQuestion bounds what a person can send.
|
||||
//
|
||||
// Not a safety control — it is a cost one. A pasted spreadsheet as a "question"
|
||||
@@ -222,6 +225,14 @@ func (s *assistantService) Ask(ctx context.Context, agentName, question string,
|
||||
}
|
||||
answer.Used = append(answer.Used, step)
|
||||
|
||||
// A resolved write ends the turn. The model is not asked to carry
|
||||
// on planning around a change that has not happened, and it is not
|
||||
// given a second chance to propose something else in the same
|
||||
// breath.
|
||||
if proposal, ok := result.Rows.(tools.Proposal); ok && err == nil {
|
||||
answer.Awaiting = &proposal
|
||||
}
|
||||
|
||||
if result.Source != "" && !seenSource[result.Source] {
|
||||
seenSource[result.Source] = true
|
||||
answer.Sources = append(answer.Sources, result.Source)
|
||||
|
||||
Reference in New Issue
Block a user