vet/agent/react.go
Abhisek Datta 5f4cccbc85
feat: Add Support for Agentic Query and Analysis (#535)
* Add initial UI for agent mode

* fix: Cleanup and define agent contract

* Add react agent

* Add interactions memory

* Add support for stdio based MCP integration

* Add basic sqlite3 report generator

* fix: Persist vulnerabilities with package relation

* fix: Persist license information

* refactor: Agents into its own command package

* feat: Add support for tool calling introspection

* refactor: UI to hide implementation detail

* sqlite3 reporter persist dependency graph

* fix: Support multiple LLM provider for agent

* docs: Update agents doc

* docs: Remove deprecated query docs

* fix: UI tests

* fix: Linter issue

* Add support for prompt mode

* Improve UI with animation

* Fix UI tests after update

* Add OpenSSF scorecard persistence

* Add slsa provenances in sqlite3 reporter

* Add test cases for sqlite3 reporter

* Fix agent doc

* fix: Sqlite3 reporter use safe accessors

* feat: Add support for fast model

* feat: Simplify and streamline agent UI for better user experience

- Remove decorative borders and excessive styling to maximize output area
- Implement clean minimal design similar to modern TUI interfaces
- Add bordered input area for clear visual separation
- Move thinking indicator above input area for better visibility
- Enhance input field reset logic for proper line alignment
- Remove verbose help text and status messages
- Optimize layout calculations for full width utilization
- Add smooth animations for agent thinking state with spinner
- Clean up code structure and remove unused progress bar functionality

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: Improve agent status line

* test: Update UI tests

* fix: Use terminal safe rendering

* fix: Fix nil deref without storing empty strings in DB

* fix: Support overwriting sqlite3 database

* fix: Data model to use m2m between manifest and package

* style: Fix linter issue with unused variables

* Misc fixes

* Add test for agent memory

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-07-11 18:37:44 +05:30

165 lines
4.1 KiB
Go

package agent
import (
"context"
"encoding/json"
"fmt"
"github.com/cloudwego/eino/components/model"
"github.com/cloudwego/eino/components/tool"
einoutils "github.com/cloudwego/eino/components/tool/utils"
"github.com/cloudwego/eino/compose"
"github.com/cloudwego/eino/flow/agent/react"
"github.com/cloudwego/eino/schema"
)
type ReactQueryAgentConfig struct {
MaxSteps int
SystemPrompt string
}
type reactQueryAgent struct {
config ReactQueryAgentConfig
model model.ToolCallingChatModel
tools []tool.BaseTool
}
var _ Agent = (*reactQueryAgent)(nil)
type reactQueryAgentOpt func(*reactQueryAgent)
func WithTools(tools []tool.BaseTool) reactQueryAgentOpt {
return func(a *reactQueryAgent) {
a.tools = tools
}
}
func NewReactQueryAgent(model model.ToolCallingChatModel,
config ReactQueryAgentConfig, opts ...reactQueryAgentOpt,
) (*reactQueryAgent, error) {
a := &reactQueryAgent{
config: config,
model: model,
}
for _, opt := range opts {
opt(a)
}
if a.config.MaxSteps == 0 {
a.config.MaxSteps = 30
}
return a, nil
}
func (a *reactQueryAgent) Execute(ctx context.Context, session Session, input Input, opts ...AgentExecutionContextOpt) (Output, error) {
executionContext := &AgentExecutionContext{}
for _, opt := range opts {
opt(executionContext)
}
agent, err := react.NewAgent(ctx, &react.AgentConfig{
ToolCallingModel: a.model,
ToolsConfig: compose.ToolsNodeConfig{
Tools: a.wrapToolsForError(a.tools),
ToolArgumentsHandler: func(ctx context.Context, name string, arguments string) (string, error) {
// Only allow introspection if the function is provided. Do not allow mutation.
if executionContext.OnToolCall != nil {
_ = executionContext.OnToolCall(ctx, session, input, name, arguments)
}
return arguments, nil
},
},
MaxStep: a.config.MaxSteps,
})
if err != nil {
return Output{}, fmt.Errorf("failed to create react agent: %w", err)
}
var messages []*schema.Message
// Start with the system prompt if available
if a.config.SystemPrompt != "" {
messages = append(messages, &schema.Message{
Role: schema.System,
Content: a.config.SystemPrompt,
})
}
// Add the previous interactions to the messages
interactions, err := session.Memory().GetInteractions(ctx)
if err != nil {
return Output{}, fmt.Errorf("failed to get session memory: %w", err)
}
// TODO: Add a limit to the number of interactions to avoid context bloat
messages = append(messages, interactions...)
// Add the current user query message to the messages
userQueryMsg := &schema.Message{
Role: schema.User,
Content: input.Query,
}
messages = append(messages, userQueryMsg)
// Execute the agent to produce a response
msg, err := agent.Generate(ctx, messages)
if err != nil {
return Output{}, fmt.Errorf("failed to generate response: %w", err)
}
// Add the user query message to the session memory
err = session.Memory().AddInteraction(ctx, userQueryMsg)
if err != nil {
return Output{}, fmt.Errorf("failed to add user query message to session memory: %w", err)
}
// Add the agent response message to the session memory
err = session.Memory().AddInteraction(ctx, msg)
if err != nil {
return Output{}, fmt.Errorf("failed to add response message to session memory: %w", err)
}
return Output{
Answer: a.schemaContent(msg),
}, nil
}
func (a *reactQueryAgent) wrapToolsForError(tools []tool.BaseTool) []tool.BaseTool {
wrappedTools := make([]tool.BaseTool, len(tools))
for i, tool := range tools {
wrappedTools[i] = einoutils.WrapToolWithErrorHandler(tool, func(_ context.Context, err error) string {
errorMessage := map[string]string{
"error": err.Error(),
"suggestion": "Tool call failed, Please try a different approach or check your input.",
}
encodedError, err := json.Marshal(errorMessage)
if err != nil {
return ""
}
return string(encodedError)
})
}
return wrappedTools
}
func (a *reactQueryAgent) schemaContent(msg *schema.Message) string {
content := msg.Content
if len(msg.MultiContent) > 0 {
content = ""
for _, part := range msg.MultiContent {
content += part.Text + "\n"
}
}
return content
}