diff --git a/docs/architecture.es.md b/docs/architecture.es.md index 098cbc3..8c89310 100644 --- a/docs/architecture.es.md +++ b/docs/architecture.es.md @@ -206,8 +206,8 @@ Bucle iterativo entre LLM y ejecución de tools. Es el "cerebro" que orquesta to ```go type Loop interface { - Run(ctx context.Context, input string) (Response, error) - RunStream(ctx context.Context, input string) iter.Seq2[Chunk, error] + Run(ctx context.Context, input string, history ...Message) (Response, error) + RunStream(ctx context.Context, input string, history ...Message) iter.Seq2[Chunk, error] } type Config struct { diff --git a/docs/architecture.md b/docs/architecture.md index eef719f..58a1450 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -206,8 +206,8 @@ Iterative loop between LLM and tool execution. It's the "brain" that orchestrate ```go type Loop interface { - Run(ctx context.Context, input string) (Response, error) - RunStream(ctx context.Context, input string) iter.Seq2[Chunk, error] + Run(ctx context.Context, input string, history ...Message) (Response, error) + RunStream(ctx context.Context, input string, history ...Message) iter.Seq2[Chunk, error] } type Config struct { diff --git a/pkg/agent/README.es.md b/pkg/agent/README.es.md index fcd7ab4..3ef6123 100644 --- a/pkg/agent/README.es.md +++ b/pkg/agent/README.es.md @@ -23,8 +23,8 @@ while iteration < MaxIterations: ```go type Loop interface { - Run(ctx context.Context, input string) (Response, error) - RunStream(ctx context.Context, input string) iter.Seq2[Chunk, error] + Run(ctx context.Context, input string, history ...llm.Message) (Response, error) + RunStream(ctx context.Context, input string, history ...llm.Message) iter.Seq2[Chunk, error] } type Config struct { diff --git a/pkg/agent/README.md b/pkg/agent/README.md index 29d52fe..a0a8dd1 100644 --- a/pkg/agent/README.md +++ b/pkg/agent/README.md @@ -20,8 +20,8 @@ while iteration < MaxIterations: ```go type Loop interface { - Run(ctx context.Context, input string) (Response, error) - RunStream(ctx context.Context, input string) iter.Seq2[Chunk, error] + Run(ctx context.Context, input string, history ...llm.Message) (Response, error) + RunStream(ctx context.Context, input string, history ...llm.Message) iter.Seq2[Chunk, error] } type Config struct { diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 2321200..914becd 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -75,10 +75,12 @@ func New(cfg Config) *Loop { } // Run executes the agent loop and returns the final response. -func (l *Loop) Run(ctx context.Context, input string) (Response, error) { +// Optional history messages are appended after the system prompt and before +// the new user input. +func (l *Loop) Run(ctx context.Context, input string, history ...llm.Message) (Response, error) { start := time.Now() - messages := l.buildInitialMessages(input) + messages := l.buildInitialMessages(input, history) var finalContent string var allToolCalls []llm.ToolCall var totalUsage llm.TokenUsage @@ -137,9 +139,11 @@ func (l *Loop) Run(ctx context.Context, input string) (Response, error) { } // RunStream executes the agent loop with streaming output. -func (l *Loop) RunStream(ctx context.Context, input string) iter.Seq2[llm.StreamChunk, error] { +// Optional history messages are appended after the system prompt and before +// the new user input. +func (l *Loop) RunStream(ctx context.Context, input string, history ...llm.Message) iter.Seq2[llm.StreamChunk, error] { return func(yield func(llm.StreamChunk, error) bool) { - messages := l.buildInitialMessages(input) + messages := l.buildInitialMessages(input, history) iterations := 0 for iterations < l.cfg.MaxIters { @@ -177,7 +181,7 @@ func (l *Loop) RunStream(ctx context.Context, input string) iter.Seq2[llm.Stream } } - if !hasToolCalls && chunk.Delta != "" { + if !hasToolCalls && (chunk.Delta != "" || chunk.ReasoningDelta != "") { responseBuilder.WriteString(chunk.Delta) if !yield(chunk, nil) { return @@ -194,12 +198,13 @@ func (l *Loop) RunStream(ctx context.Context, input string) iter.Seq2[llm.Stream } } -func (l *Loop) buildInitialMessages(input string) []llm.Message { +func (l *Loop) buildInitialMessages(input string, history []llm.Message) []llm.Message { systemPrompt := persona.AssembleSystemPrompt(l.cfg.Persona, "") - return []llm.Message{ - {Role: llm.RoleSystem, Content: systemPrompt}, - {Role: llm.RoleUser, Content: input}, - } + messages := make([]llm.Message, 0, len(history)+2) + messages = append(messages, llm.Message{Role: llm.RoleSystem, Content: systemPrompt}) + messages = append(messages, history...) + messages = append(messages, llm.Message{Role: llm.RoleUser, Content: input}) + return messages } func (l *Loop) getToolSchemas() []json.RawMessage { diff --git a/pkg/llm/providers/llamacpp/client.go b/pkg/llm/providers/llamacpp/client.go index 0135ca9..425e9d8 100644 --- a/pkg/llm/providers/llamacpp/client.go +++ b/pkg/llm/providers/llamacpp/client.go @@ -124,7 +124,8 @@ func (c *Client) Stream(ctx context.Context, req llm.CompletionRequest) iter.Seq for _, choice := range event.Choices { chunk := llm.StreamChunk{ - Delta: choice.Delta.Content, + Delta: choice.Delta.Content, + ReasoningDelta: choice.Delta.ReasoningContent, } if choice.FinishReason != "" { chunk.FinishReason = choice.FinishReason @@ -210,6 +211,7 @@ func (c *Client) toResponse(resp llamaChatResponse) llm.CompletionResponse { ID: resp.ID, Model: resp.Model, Content: choice.Message.Content, + Reasoning: choice.Message.ReasoningContent, StopReason: choice.FinishReason, } @@ -270,9 +272,10 @@ type llamaChoice struct { } type llamaMessageResult struct { - Role string `json:"role"` - Content string `json:"content"` - ToolCalls []llamaToolCall `json:"tool_calls"` + Role string `json:"role"` + Content string `json:"content"` + ReasoningContent string `json:"reasoning_content"` + ToolCalls []llamaToolCall `json:"tool_calls"` } type llamaToolCall struct { @@ -306,9 +309,10 @@ type llamaStreamChoice struct { } type llamaStreamDelta struct { - Content string `json:"content"` - Role string `json:"role"` - ToolCalls []llamaStreamToolCall `json:"tool_calls"` + Content string `json:"content"` + ReasoningContent string `json:"reasoning_content"` + Role string `json:"role"` + ToolCalls []llamaStreamToolCall `json:"tool_calls"` } type llamaStreamToolCall struct { diff --git a/pkg/llm/types.go b/pkg/llm/types.go index 366a9c2..f9f222d 100644 --- a/pkg/llm/types.go +++ b/pkg/llm/types.go @@ -82,6 +82,7 @@ type CompletionResponse struct { ID string `json:"id"` Model string `json:"model"` Content string `json:"content"` + Reasoning string `json:"reasoning,omitempty"` ToolCalls []ToolCall `json:"tool_calls,omitempty"` StopReason string `json:"stop_reason"` Usage TokenUsage `json:"usage,omitempty"` @@ -105,10 +106,11 @@ type ToolCall struct { // StreamChunk is emitted by the iterator returned from Stream(). type StreamChunk struct { - Delta string `json:"delta"` - ToolCalls []ToolCall `json:"tool_calls,omitempty"` - FinishReason string `json:"finish_reason,omitempty"` - Usage TokenUsage `json:"usage,omitempty"` // only present on final chunk + Delta string `json:"delta"` + ReasoningDelta string `json:"reasoning_delta,omitempty"` + ToolCalls []ToolCall `json:"tool_calls,omitempty"` + FinishReason string `json:"finish_reason,omitempty"` + Usage TokenUsage `json:"usage,omitempty"` // only present on final chunk } // LLMClient is the interface that every provider implements. diff --git a/pkg/persona/persona.go b/pkg/persona/persona.go index d7706f0..0d83982 100644 --- a/pkg/persona/persona.go +++ b/pkg/persona/persona.go @@ -37,7 +37,7 @@ func DefaultPersona() Persona { Name: "Rony", Tone: "professional and helpful", Style: "clear and concise", - Language: "en", + Language: "the user's language", } } @@ -59,7 +59,11 @@ func AssembleSystemPrompt(p Persona, agentsMD string) string { parts = append(parts, fmt.Sprintf("Write in a %s style.", p.Style)) } if p.Language != "" { - parts = append(parts, fmt.Sprintf("Respond in %s.", p.Language)) + if strings.EqualFold(p.Language, "the user's language") { + parts = append(parts, "Respond in the same language as the user's messages.") + } else { + parts = append(parts, fmt.Sprintf("Respond in %s.", p.Language)) + } } for _, c := range p.Constraints { parts = append(parts, fmt.Sprintf("CONSTRAINT: %s", c))