Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ module github.com/strrl/lapp
go 1.25.7

require (
github.com/bytedance/sonic v1.15.0
github.com/cloudwego/eino v0.8.0
github.com/cloudwego/eino-ext/adk/backend/local v0.1.2-0.20260306073537-008f82264d85
github.com/cloudwego/eino-ext/callbacks/langfuse v0.0.0-20260227151421-e109b4ff9563
github.com/cloudwego/eino-ext/components/model/openrouter v0.1.2
github.com/duckdb/duckdb-go/v2 v2.5.5
Expand All @@ -24,10 +24,8 @@ require (
require (
github.com/apache/arrow-go/v18 v18.5.1 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
Expand Down
4 changes: 0 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ github.com/apache/thrift v0.22.0/go.mod h1:1e7J/O1Ae6ZQMTYdy9xa3w9k+XHWPfRvdPyJe
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
Expand All @@ -34,8 +32,6 @@ github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cloudwego/eino v0.8.0 h1:DLbrgEAloA+l7aR2qim7qQocQB48DjPrb8LzG3PYMHY=
github.com/cloudwego/eino v0.8.0/go.mod h1:+2N4nsMPxA6kGBHpH+75JuTfEcGprAMTdsZESrShKpU=
github.com/cloudwego/eino-ext/adk/backend/local v0.1.2-0.20260306073537-008f82264d85 h1:mD47o0GKdeqMdGI5xEqnlO8ZtArvhalIorRtrCmLRkA=
github.com/cloudwego/eino-ext/adk/backend/local v0.1.2-0.20260306073537-008f82264d85/go.mod h1:LfFk+VqZk0JOxIyl5RaerYqlFVLyXOCoSaqqak8hNls=
github.com/cloudwego/eino-ext/callbacks/langfuse v0.0.0-20260227151421-e109b4ff9563 h1:DKTXDDw8ErC4RorZLfB2ZdHChjDKWIqOEO7VRSjjfbg=
github.com/cloudwego/eino-ext/callbacks/langfuse v0.0.0-20260227151421-e109b4ff9563/go.mod h1:lrNKITZR4QUaYl9Rdz9W6qGOolHRy6mPamEZYA8uz7s=
github.com/cloudwego/eino-ext/components/model/openrouter v0.1.2 h1:zDFteouktUsGk4I/7m1b7yT4e9qawy45gWtLoyeHwxI=
Expand Down
10 changes: 9 additions & 1 deletion pkg/analyzer/acp_tool_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ package analyzer
import (
"context"

"github.com/cloudwego/eino/components"
"github.com/cloudwego/eino/components/model"
"github.com/cloudwego/eino/schema"
einoacp "github.com/strrl/eino-acp"
)

var _ model.ToolCallingChatModel = (*acpToolCallingModel)(nil)
var (
_ model.ToolCallingChatModel = (*acpToolCallingModel)(nil)
_ components.Checker = (*acpToolCallingModel)(nil)
)

// acpToolCallingModel adapts eino-acp ChatModel to ToolCallingChatModel.
// ACP agents manage tools in their own runtime, so WithTools is a no-op.
Expand All @@ -20,6 +24,10 @@ func newACPToolCallingModel(base *einoacp.ChatModel) model.ToolCallingChatModel
return &acpToolCallingModel{base: base}
}

func (m *acpToolCallingModel) IsCallbacksEnabled() bool {
return true
}

func (m *acpToolCallingModel) Generate(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) {
return m.base.Generate(ctx, input, opts...)
}
Expand Down
58 changes: 26 additions & 32 deletions pkg/analyzer/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,30 @@ import (
"path/filepath"
"strings"

"github.com/cloudwego/eino-ext/adk/backend/local"
"github.com/cloudwego/eino/adk"
fsmw "github.com/cloudwego/eino/adk/middlewares/filesystem"
"github.com/go-errors/errors"
"github.com/google/uuid"
einoacp "github.com/strrl/eino-acp"
"github.com/strrl/lapp/pkg/tape"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"

"github.com/strrl/lapp/pkg/tape"
"github.com/strrl/lapp/pkg/tracing"
)

func buildSystemPrompt(workDir string) string {
return fmt.Sprintf(`You are a log analysis expert helping developers troubleshoot issues.

IMPORTANT: All file operations (read_file, grep, ls, glob, execute) MUST use paths under %s.
Do NOT access files outside this workspace directory.
IMPORTANT: Stay within the workspace directory %s for any file or shell work (your runtime provides the tools).

Your workspace contains pre-processed log data at %s:
- %s/raw.log — the original log file
- %s/summary.txt — log templates discovered by automated parsing, with occurrence counts and samples
- %s/errors.txt — error and warning patterns extracted from logs

Start by reading %s/summary.txt and %s/errors.txt to understand the log patterns.
Then use grep and read_file on %s/raw.log to investigate specific patterns in detail.
You can also use the execute tool to run shell commands (e.g., awk, sort, wc) for deeper analysis.
Then search and read %s/raw.log for specifics (grep, read, or equivalents your environment exposes).
Use shell only when it helps (e.g. awk, sort, wc).

Provide:
1. Key findings from the logs
Expand All @@ -46,16 +46,15 @@ Be concise and actionable. Focus on what matters.`,
type Config struct {
Provider string
Model string
// TapePath, when set, enables tape recording to this JSONL file.
// TapePath overrides the default workspace tape file.
TapePath string
}

// BuildWorkspaceSystemPrompt builds a system prompt for the structured workspace layout.
func BuildWorkspaceSystemPrompt(workDir string) string {
return fmt.Sprintf(`You are a log analysis expert helping developers troubleshoot issues.

IMPORTANT: All file operations (read_file, grep, ls, glob, execute) MUST use paths under %s.
Do NOT access files outside this workspace directory.
IMPORTANT: Stay within the workspace directory %s for any file or shell work (your runtime provides the tools).

Your workspace at %s contains structured log data:
- %s/logs/ — original log files
Expand All @@ -67,8 +66,8 @@ Your workspace at %s contains structured log data:

Start by reading %s/notes/summary.md and %s/notes/errors.md to understand the log patterns.
Then drill into specific patterns under %s/patterns/ for details.
Use grep on %s/logs/ to search for specific terms across all log files.
You can also use the execute tool to run shell commands (e.g., awk, sort, wc) for deeper analysis.
Search %s/logs/ for specific terms across log files.
Use shell only when it helps (e.g., awk, sort, wc).

Provide:
1. Key findings from the logs
Expand Down Expand Up @@ -117,30 +116,31 @@ func RunAgentWithPrompt(ctx context.Context, config Config, workDir, question, s
return "", errors.Errorf("create chat model: %w", err)
}

backend, err := local.NewBackend(ctx, &local.Config{})
if err != nil {
return "", errors.Errorf("create local backend: %w", err)
if systemPrompt == "" {
systemPrompt = buildSystemPrompt(absDir)
}
backendAdapter := newLocalBackendAdapter(backend)

fsHandler, err := fsmw.New(ctx, &fsmw.MiddlewareConfig{
Backend: backendAdapter,
StreamingShell: backendAdapter,
})
tapePath := config.TapePath
if tapePath == "" {
tapePath = filepath.Join(absDir, tape.FileName)
}
tapeStore, err := tape.OpenJSONL(tapePath)
if err != nil {
return "", errors.Errorf("create filesystem middleware: %w", err)
return "", errors.Errorf("open tape store: %w", err)
}
defer func() { _ = tapeStore.Close() }()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Wait for streamed tape callbacks before closing

When ACP emits streamed callbacks, pkg/tape/eino_handler.go drains and writes the stream from a goroutine, but this deferred close runs as soon as the runner iterator finishes. There is no synchronization with those goroutines, so the final assistant message/run event can race with Close() and be dropped with an append error, leaving .tape.jsonl incomplete for normal streamed model responses.

Useful? React with 👍 / 👎.


if systemPrompt == "" {
systemPrompt = buildSystemPrompt(absDir)
}
tapeHandler := tape.NewEinoHandler(tapeStore, tape.RunMeta{
RunID: uuid.NewString(),
Provider: provider,
Model: config.Model,
})

agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
Name: "log-analyzer",
Description: "Analyzes log files to find root causes",
Instruction: systemPrompt,
Model: newACPToolCallingModel(chatModel),
Handlers: []adk.ChatModelAgentMiddleware{fsHandler},
MaxIterations: 15,
})
if err != nil {
Expand All @@ -153,13 +153,7 @@ func RunAgentWithPrompt(ctx context.Context, config Config, workDir, question, s
}

runner := adk.NewRunner(ctx, adk.RunnerConfig{Agent: agent})
var runOptions []adk.AgentRunOption
if config.TapePath != "" {
jsonlStore := tape.NewJSONLStore(config.TapePath)
runOptions = append(runOptions, adk.WithCallbacks(tape.NewHandler(jsonlStore)))
slog.Info("Tape recording enabled", "path", config.TapePath)
}
iter := runner.Query(ctx, userMessage, runOptions...)
iter := runner.Query(ctx, userMessage, adk.WithCallbacks(tracing.NewSlogEinoHandler(nil), tapeHandler))

var result strings.Builder
for {
Expand Down
52 changes: 0 additions & 52 deletions pkg/analyzer/local_backend_adapter.go

This file was deleted.

Loading
Loading