released the mock
This commit is contained in:
+31
-21
@@ -117,6 +117,27 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
m.width = msg.Width
|
||||||
|
m.height = msg.Height
|
||||||
|
|
||||||
|
headerHeight := 1
|
||||||
|
inputHeight := 5 // Textarea height(3) + border/padding
|
||||||
|
|
||||||
|
vpWidth := msg.Width - 4 // Account for viewportStyle padding and border
|
||||||
|
vpHeight := msg.Height - headerHeight - inputHeight - 2
|
||||||
|
|
||||||
|
if !m.ready {
|
||||||
|
m.viewport = viewport.New(vpWidth, vpHeight)
|
||||||
|
m.viewport.SetContent(strings.Join(m.messages, "\n\n"))
|
||||||
|
m.ready = true
|
||||||
|
} else {
|
||||||
|
m.viewport.Width = vpWidth
|
||||||
|
m.viewport.Height = vpHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
m.textarea.SetWidth(msg.Width - 4)
|
||||||
|
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
switch msg.Type {
|
switch msg.Type {
|
||||||
case tea.KeyCtrlC, tea.KeyEsc:
|
case tea.KeyCtrlC, tea.KeyEsc:
|
||||||
@@ -129,40 +150,29 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
if strings.TrimSpace(v) == "" || m.isThinking {
|
if strings.TrimSpace(v) == "" || m.isThinking {
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
m.messages = append(m.messages, userStyle.Render("You")+"\n"+v)
|
|
||||||
|
wrappedUser := lipgloss.NewStyle().Width(m.viewport.Width).Render(userStyle.Render("You") + "\n" + v)
|
||||||
|
m.messages = append(m.messages, wrappedUser)
|
||||||
m.textarea.Reset()
|
m.textarea.Reset()
|
||||||
m.viewport.SetContent(strings.Join(m.messages, "\n\n"))
|
m.viewport.SetContent(strings.Join(m.messages, "\n\n"))
|
||||||
m.viewport.GotoBottom()
|
m.viewport.GotoBottom()
|
||||||
m.isThinking = true
|
m.isThinking = true
|
||||||
return m, m.runAgent(v)
|
return m, m.runAgent(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
case agentResponseMsg:
|
case agentResponseMsg:
|
||||||
m.isThinking = false
|
m.isThinking = false
|
||||||
|
var newMsg string
|
||||||
if msg.err != nil {
|
if msg.err != nil {
|
||||||
m.messages = append(m.messages, sysStyle.Render(fmt.Sprintf("Error: %v", msg.err)))
|
newMsg = sysStyle.Render(fmt.Sprintf("Error: %v", msg.err))
|
||||||
} else {
|
} else {
|
||||||
m.messages = append(m.messages, agentStyle.Render("Sidekick")+"\n"+msg.response)
|
newMsg = agentStyle.Render("Sidekick") + "\n" + msg.response
|
||||||
}
|
}
|
||||||
|
wrappedMsg := lipgloss.NewStyle().Width(m.viewport.Width).Render(newMsg)
|
||||||
|
m.messages = append(m.messages, wrappedMsg)
|
||||||
m.viewport.SetContent(strings.Join(m.messages, "\n\n"))
|
m.viewport.SetContent(strings.Join(m.messages, "\n\n"))
|
||||||
m.viewport.GotoBottom()
|
m.viewport.GotoBottom()
|
||||||
return m, nil
|
return m, nil
|
||||||
case tea.WindowSizeMsg:
|
|
||||||
m.width = msg.Width
|
|
||||||
m.height = msg.Height
|
|
||||||
|
|
||||||
headerHeight := 1
|
|
||||||
inputHeight := 5 // Textarea height(3) + border/padding
|
|
||||||
|
|
||||||
if !m.ready {
|
|
||||||
m.viewport = viewport.New(msg.Width-2, msg.Height-headerHeight-inputHeight-2)
|
|
||||||
m.viewport.SetContent(strings.Join(m.messages, "\n\n"))
|
|
||||||
m.ready = true
|
|
||||||
} else {
|
|
||||||
m.viewport.Width = msg.Width - 2
|
|
||||||
m.viewport.Height = msg.Height - headerHeight - inputHeight - 2
|
|
||||||
}
|
|
||||||
|
|
||||||
m.textarea.SetWidth(msg.Width - 4)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
m.textarea, tiCmd = m.textarea.Update(msg)
|
m.textarea, tiCmd = m.textarea.Update(msg)
|
||||||
@@ -192,7 +202,7 @@ func (m model) View() string {
|
|||||||
statusStyle.Render(" "+status+" "),
|
statusStyle.Render(" "+status+" "),
|
||||||
)
|
)
|
||||||
|
|
||||||
vpView := viewportStyle.Width(m.width - 2).Height(m.height - 10).Render(m.viewport.View())
|
vpView := viewportStyle.Width(m.width - 2).Render(m.viewport.View())
|
||||||
|
|
||||||
taStyle := textareaStyle
|
taStyle := textareaStyle
|
||||||
if m.textarea.Focused() {
|
if m.textarea.Focused() {
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/viewport"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -54,3 +56,18 @@ func TestModelUpdate(t *testing.T) {
|
|||||||
t.Error("expected message list to grow after response")
|
t.Error("expected message list to grow after response")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMessageWrapping(t *testing.T) {
|
||||||
|
m := initialModel()
|
||||||
|
m.ready = true
|
||||||
|
m.viewport = viewport.New(10, 5) // Very narrow
|
||||||
|
|
||||||
|
longMsg := "This is a very long message that should be wrapped."
|
||||||
|
respModel, _ := m.Update(agentResponseMsg{response: longMsg})
|
||||||
|
rm := respModel.(model)
|
||||||
|
|
||||||
|
lastMsg := rm.messages[len(rm.messages)-1]
|
||||||
|
if !strings.Contains(lastMsg, "\n") {
|
||||||
|
t.Errorf("expected long message to be wrapped, but no newline found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+95
@@ -0,0 +1,95 @@
|
|||||||
|
tool_response_threshold = 10000
|
||||||
|
|
||||||
|
[mcp_listener]
|
||||||
|
transport = "http"
|
||||||
|
port = 8080
|
||||||
|
|
||||||
|
[models]
|
||||||
|
[models.default]
|
||||||
|
model_id = "z-ai/glm5"
|
||||||
|
endpoint = "https://integrate.api.nvidia.com/v1"
|
||||||
|
api_key = "env:SK_NV_API_KEY"
|
||||||
|
temperature = 0.0
|
||||||
|
max_tokens = 4096
|
||||||
|
timeout_secs = 120
|
||||||
|
|
||||||
|
[models.fast]
|
||||||
|
model_id = "minimaxai/minimax-m2.5"
|
||||||
|
endpoint = "https://integrate.api.nvidia.com/v1"
|
||||||
|
api_key = "env:SK_NV_API_KEY"
|
||||||
|
temperature = 0.1
|
||||||
|
max_tokens = 1024
|
||||||
|
timeout_secs = 60
|
||||||
|
|
||||||
|
[mcp_servers]
|
||||||
|
[mcp_servers.filesystem]
|
||||||
|
transport = "stdio"
|
||||||
|
command = "npx"
|
||||||
|
args = ["-y", "@modelcontextprotocol/server-filesystem", "."]
|
||||||
|
env = []
|
||||||
|
|
||||||
|
[mcp_servers.context7]
|
||||||
|
transport = "http"
|
||||||
|
url = "https://mcp.context7.com/mcp"
|
||||||
|
|
||||||
|
[mcp_servers.exa]
|
||||||
|
transport = "stdio"
|
||||||
|
command = "npx"
|
||||||
|
args = ["-y", "@exa/mcp-server"]
|
||||||
|
|
||||||
|
[agents]
|
||||||
|
[agents.coordinator]
|
||||||
|
role = "COORDINATOR"
|
||||||
|
model_id = "default"
|
||||||
|
enable_shell_exec = true
|
||||||
|
system_prompt = "You are the Lead Architect. You oversee the software development lifecycle, plan features, and delegate implementation to specialized subagents."
|
||||||
|
description = "Lead Architect and project coordinator"
|
||||||
|
max_iterations = 15
|
||||||
|
subagents = ["coder", "reviewer", "tester"]
|
||||||
|
toolsets = ["filesystem", "context7", "exa"]
|
||||||
|
goals = [
|
||||||
|
"Coordinate complex software engineering tasks",
|
||||||
|
"Ensure high-quality code delivery by delegating to specialized agents",
|
||||||
|
"Validate the final solution using shell execution and test results"
|
||||||
|
]
|
||||||
|
|
||||||
|
[agents.coder]
|
||||||
|
role = "SPECIALIST"
|
||||||
|
model_id = "default"
|
||||||
|
enable_shell_exec = true
|
||||||
|
system_prompt = "You are a Senior Software Engineer specializing in implementation. Your goal is to write clean, efficient, and well-documented code based on the architect's instructions."
|
||||||
|
description = "Senior Developer focused on implementation and refactoring"
|
||||||
|
max_iterations = 10
|
||||||
|
toolsets = ["filesystem", "context7"]
|
||||||
|
goals = [
|
||||||
|
"Implement features and bug fixes with precision",
|
||||||
|
"Adhere to project-specific coding standards and best practices",
|
||||||
|
"Verify changes by running builds or small code snippets"
|
||||||
|
]
|
||||||
|
|
||||||
|
[agents.reviewer]
|
||||||
|
role = "SPECIALIST"
|
||||||
|
model_id = "fast"
|
||||||
|
system_prompt = "You are a Quality Assurance Specialist and Code Reviewer. Your role is to analyze code for potential bugs, security vulnerabilities, and style violations."
|
||||||
|
description = "Code Reviewer and Quality Assurance specialist"
|
||||||
|
max_iterations = 8
|
||||||
|
toolsets = ["filesystem", "context7"]
|
||||||
|
goals = [
|
||||||
|
"Perform thorough code reviews and identify edge cases",
|
||||||
|
"Suggest optimizations and improvements",
|
||||||
|
"Use search tools and context7 for deep library/best practice research"
|
||||||
|
]
|
||||||
|
|
||||||
|
[agents.tester]
|
||||||
|
role = "SPECIALIST"
|
||||||
|
model_id = "fast"
|
||||||
|
enable_shell_exec = true
|
||||||
|
system_prompt = "You are a Test Engineer. Your primary responsibility is to write and execute tests to ensure software reliability and correctness."
|
||||||
|
description = "Test Engineer focused on automated testing and verification"
|
||||||
|
max_iterations = 10
|
||||||
|
toolsets = ["filesystem"]
|
||||||
|
goals = [
|
||||||
|
"Develop comprehensive test suites (unit, integration, e2e)",
|
||||||
|
"Run tests and analyze failures to provide actionable feedback",
|
||||||
|
"Ensure 100% verification of implemented features"
|
||||||
|
]
|
||||||
@@ -1,30 +1,112 @@
|
|||||||
package sidekick
|
package sidekick
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LLMClient represents an abstraction over the LLM provider
|
// LLMClient represents an abstraction over the LLM provider
|
||||||
type LLMClient interface {
|
type LLMClient interface {
|
||||||
Call(ctx context.Context, model ModelConfig, messages []Message, tools map[string]ToolDefinition) (Message, error)
|
Call(ctx context.Context, model ModelConfig, messages []Message, tools map[string]ToolDefinition) (Message, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultLLMClient LLMClient = &mockLLMClient{}
|
// standardLLMClient is an OpenAI-compatible implementation
|
||||||
|
type standardLLMClient struct{}
|
||||||
|
|
||||||
|
func (s *standardLLMClient) Call(ctx context.Context, model ModelConfig, msgs []Message, ts map[string]ToolDefinition) (Message, error) {
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: model.Timeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
reqBody := map[string]interface{}{
|
||||||
|
"model": model.ModelID,
|
||||||
|
"messages": msgs,
|
||||||
|
"temperature": model.Temperature,
|
||||||
|
}
|
||||||
|
if model.MaxTokens > 0 {
|
||||||
|
reqBody["max_tokens"] = model.MaxTokens
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ts) > 0 {
|
||||||
|
var tools []map[string]interface{}
|
||||||
|
for _, t := range ts {
|
||||||
|
tools = append(tools, t.ConvertToOpenAITool())
|
||||||
|
}
|
||||||
|
reqBody["tools"] = tools
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonBody, err := json.Marshal(reqBody)
|
||||||
|
if err != nil {
|
||||||
|
return Message{}, fmt.Errorf("failed to marshal request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "POST", model.Endpoint+"/chat/completions", bytes.NewBuffer(jsonBody))
|
||||||
|
if err != nil {
|
||||||
|
return Message{}, fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Authorization", "Bearer "+model.APIKey)
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return Message{}, fmt.Errorf("HTTP request failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return Message{}, fmt.Errorf("failed to read response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return Message{}, fmt.Errorf("API returned error (%d): %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
var openAIResp struct {
|
||||||
|
Choices []struct {
|
||||||
|
Message Message `json:"message"`
|
||||||
|
} `json:"choices"`
|
||||||
|
Error *struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
} `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(body, &openAIResp); err != nil {
|
||||||
|
return Message{}, fmt.Errorf("failed to unmarshal response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if openAIResp.Error != nil {
|
||||||
|
return Message{}, fmt.Errorf("API error: %s", openAIResp.Error.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(openAIResp.Choices) == 0 {
|
||||||
|
return Message{}, fmt.Errorf("API returned no choices")
|
||||||
|
}
|
||||||
|
|
||||||
|
return openAIResp.Choices[0].Message, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultLLMClient LLMClient = &standardLLMClient{}
|
||||||
|
|
||||||
// SetLLMClient allows overriding the LLM client (e.g. for testing)
|
// SetLLMClient allows overriding the LLM client (e.g. for testing)
|
||||||
func SetLLMClient(client LLMClient) {
|
func SetLLMClient(client LLMClient) {
|
||||||
defaultLLMClient = client
|
defaultLLMClient = client
|
||||||
}
|
}
|
||||||
|
|
||||||
// CallLLM invokes the configured LLM client
|
// CallLLM invokes the configured LLM client
|
||||||
func CallLLM(ctx context.Context, model ModelConfig, msgs []Message, tools map[string]ToolDefinition) (Message, error) {
|
func CallLLM(ctx context.Context, model ModelConfig, msgs []Message, tools map[string]ToolDefinition) (Message, error) {
|
||||||
return defaultLLMClient.Call(ctx, model, msgs, tools)
|
return defaultLLMClient.Call(ctx, model, msgs, tools)
|
||||||
}
|
}
|
||||||
|
|
||||||
// mockLLMClient is used as a fallback if no actual HTTP client is injected.
|
// mockLLMClient is used as a fallback if no actual HTTP client is injected.
|
||||||
type mockLLMClient struct {
|
type mockLLMClient struct {
|
||||||
responses []Message
|
responses []Message
|
||||||
calls int
|
calls int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockLLMClient) Call(ctx context.Context, model ModelConfig, msgs []Message, ts map[string]ToolDefinition) (Message, error) {
|
func (m *mockLLMClient) Call(ctx context.Context, model ModelConfig, msgs []Message, ts map[string]ToolDefinition) (Message, error) {
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# Sidekick Multiline System Prompts Example
|
||||||
|
# Use {{template "name"}} to include other prompts as subprompts.
|
||||||
|
|
||||||
|
base_identity = """
|
||||||
|
You are a highly capable AI software engineer operating within the Sidekick framework.
|
||||||
|
Your role is to assist the user by following instructions precisely and using
|
||||||
|
the available tools or subagents effectively. Always be professional, concise,
|
||||||
|
and technical.
|
||||||
|
"""
|
||||||
|
|
||||||
|
base_formatting = """
|
||||||
|
When using tools:
|
||||||
|
1. State your reasoning for calling the tool.
|
||||||
|
2. Formulate the JSON action with the correct parameters.
|
||||||
|
3. If a tool result is buffered to a file, use read_file or grep_file to inspect it.
|
||||||
|
|
||||||
|
When responding to the user:
|
||||||
|
- Use Markdown for formatting and code blocks with appropriate language tags.
|
||||||
|
- Be direct, technical, and precise.
|
||||||
|
"""
|
||||||
|
|
||||||
|
coding_standards = """
|
||||||
|
Adhere to these universal coding standards:
|
||||||
|
- Use clear, descriptive variable and function names.
|
||||||
|
- Keep functions small and focused on a single responsibility.
|
||||||
|
- Include meaningful comments for complex logic.
|
||||||
|
- Ensure proper error handling and logging.
|
||||||
|
"""
|
||||||
|
|
||||||
|
coordinator = """
|
||||||
|
{{template "base_identity"}}
|
||||||
|
{{template "base_formatting"}}
|
||||||
|
|
||||||
|
You are the COORDINATOR (Lead Architect). You are the primary point of contact for the user.
|
||||||
|
Your workflow:
|
||||||
|
1. Analyze the user's request and map out the necessary changes.
|
||||||
|
2. Use the 'coder' subagent to implement the changes.
|
||||||
|
3. Use the 'reviewer' subagent to verify the code quality.
|
||||||
|
4. Use the 'tester' subagent to ensure everything works as expected.
|
||||||
|
5. If any step fails, iterate with the relevant subagent.
|
||||||
|
|
||||||
|
You have full shell access to run builds and orchestrate the entire process.
|
||||||
|
"""
|
||||||
|
|
||||||
|
coder = """
|
||||||
|
{{template "base_identity"}}
|
||||||
|
{{template "base_formatting"}}
|
||||||
|
{{template "coding_standards"}}
|
||||||
|
|
||||||
|
You are the CODER specialist. Your task is to implement specific features or fixes
|
||||||
|
as directed by the Architect. You should focus on writing the actual code and
|
||||||
|
ensuring it compiles/runs. Use shell_exec sparingly to verify your changes if necessary.
|
||||||
|
"""
|
||||||
|
|
||||||
|
reviewer = """
|
||||||
|
{{template "base_identity"}}
|
||||||
|
{{template "base_formatting"}}
|
||||||
|
|
||||||
|
You are the REVIEWER specialist. Your goal is to find flaws.
|
||||||
|
Do not just look for syntax errors; look for logical bugs, inefficient algorithms,
|
||||||
|
and potential security risks. Compare the implementation against the requested
|
||||||
|
requirements and coding standards.
|
||||||
|
"""
|
||||||
|
|
||||||
|
tester = """
|
||||||
|
{{template "base_identity"}}
|
||||||
|
{{template "base_formatting"}}
|
||||||
|
|
||||||
|
You are the TESTER specialist. You must ensure the software is bulletproof.
|
||||||
|
Write test cases that cover both happy paths and edge cases.
|
||||||
|
Use shell_exec to run the project's test suite and report results back
|
||||||
|
to the Architect.
|
||||||
|
"""
|
||||||
Reference in New Issue
Block a user