fantasy-cli 1.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +456 -0
- data/bin/gsd +8 -0
- data/bin/gsd-core-darwin-amd64 +0 -0
- data/bin/gsd-core-darwin-arm64 +0 -0
- data/bin/gsd-core-linux-amd64 +0 -0
- data/bin/gsd-core-linux-arm64 +0 -0
- data/bin/gsd-core-windows-amd64.exe +0 -0
- data/bin/gsd-core-windows-arm64.exe +0 -0
- data/bin/gsd-core.exe +0 -0
- data/lib/gsd/agents/coordinator.rb +195 -0
- data/lib/gsd/agents/task_manager.rb +158 -0
- data/lib/gsd/agents/worker.rb +162 -0
- data/lib/gsd/agents.rb +30 -0
- data/lib/gsd/ai/chat.rb +486 -0
- data/lib/gsd/ai/cli.rb +248 -0
- data/lib/gsd/ai/command_parser.rb +97 -0
- data/lib/gsd/ai/commands/base.rb +42 -0
- data/lib/gsd/ai/commands/clear.rb +20 -0
- data/lib/gsd/ai/commands/context.rb +30 -0
- data/lib/gsd/ai/commands/cost.rb +30 -0
- data/lib/gsd/ai/commands/export.rb +42 -0
- data/lib/gsd/ai/commands/help.rb +61 -0
- data/lib/gsd/ai/commands/model.rb +67 -0
- data/lib/gsd/ai/commands/reset.rb +22 -0
- data/lib/gsd/ai/config.rb +256 -0
- data/lib/gsd/ai/context.rb +324 -0
- data/lib/gsd/ai/cost_tracker.rb +361 -0
- data/lib/gsd/ai/git_context.rb +169 -0
- data/lib/gsd/ai/history.rb +384 -0
- data/lib/gsd/ai/providers/anthropic.rb +429 -0
- data/lib/gsd/ai/providers/base.rb +282 -0
- data/lib/gsd/ai/providers/lmstudio.rb +279 -0
- data/lib/gsd/ai/providers/ollama.rb +336 -0
- data/lib/gsd/ai/providers/openai.rb +396 -0
- data/lib/gsd/ai/providers/openrouter.rb +429 -0
- data/lib/gsd/ai/reference_resolver.rb +225 -0
- data/lib/gsd/ai/repl.rb +349 -0
- data/lib/gsd/ai/streaming.rb +438 -0
- data/lib/gsd/ai/ui.rb +429 -0
- data/lib/gsd/buddy/cli.rb +284 -0
- data/lib/gsd/buddy/gacha.rb +148 -0
- data/lib/gsd/buddy/renderer.rb +108 -0
- data/lib/gsd/buddy/species.rb +190 -0
- data/lib/gsd/buddy/stats.rb +156 -0
- data/lib/gsd/buddy.rb +28 -0
- data/lib/gsd/cli.rb +455 -0
- data/lib/gsd/commands.rb +198 -0
- data/lib/gsd/config.rb +183 -0
- data/lib/gsd/error.rb +188 -0
- data/lib/gsd/frontmatter.rb +123 -0
- data/lib/gsd/go/bridge.rb +173 -0
- data/lib/gsd/history.rb +76 -0
- data/lib/gsd/milestone.rb +75 -0
- data/lib/gsd/output.rb +184 -0
- data/lib/gsd/phase.rb +102 -0
- data/lib/gsd/plugins/base.rb +92 -0
- data/lib/gsd/plugins/cli.rb +330 -0
- data/lib/gsd/plugins/config.rb +164 -0
- data/lib/gsd/plugins/hooks.rb +132 -0
- data/lib/gsd/plugins/installer.rb +158 -0
- data/lib/gsd/plugins/loader.rb +122 -0
- data/lib/gsd/plugins/manager.rb +187 -0
- data/lib/gsd/plugins/marketplace.rb +142 -0
- data/lib/gsd/plugins/sandbox.rb +114 -0
- data/lib/gsd/plugins/search.rb +131 -0
- data/lib/gsd/plugins/validator.rb +157 -0
- data/lib/gsd/plugins.rb +48 -0
- data/lib/gsd/profile.rb +127 -0
- data/lib/gsd/research.rb +85 -0
- data/lib/gsd/roadmap.rb +90 -0
- data/lib/gsd/skills/bundled/commit.md +58 -0
- data/lib/gsd/skills/bundled/debug.md +28 -0
- data/lib/gsd/skills/bundled/explain.md +41 -0
- data/lib/gsd/skills/bundled/plan.md +42 -0
- data/lib/gsd/skills/bundled/verify.md +26 -0
- data/lib/gsd/skills/loader.rb +189 -0
- data/lib/gsd/state.rb +102 -0
- data/lib/gsd/template.rb +106 -0
- data/lib/gsd/tools/ask_user_question.rb +179 -0
- data/lib/gsd/tools/base.rb +204 -0
- data/lib/gsd/tools/bash.rb +246 -0
- data/lib/gsd/tools/file_edit.rb +297 -0
- data/lib/gsd/tools/file_read.rb +199 -0
- data/lib/gsd/tools/file_write.rb +153 -0
- data/lib/gsd/tools/glob.rb +202 -0
- data/lib/gsd/tools/grep.rb +227 -0
- data/lib/gsd/tools/gsd_frontmatter.rb +165 -0
- data/lib/gsd/tools/gsd_phase.rb +140 -0
- data/lib/gsd/tools/gsd_roadmap.rb +108 -0
- data/lib/gsd/tools/gsd_state.rb +143 -0
- data/lib/gsd/tools/gsd_template.rb +157 -0
- data/lib/gsd/tools/gsd_verify.rb +159 -0
- data/lib/gsd/tools/registry.rb +103 -0
- data/lib/gsd/tools/task.rb +235 -0
- data/lib/gsd/tools/todo_write.rb +290 -0
- data/lib/gsd/tools/web.rb +260 -0
- data/lib/gsd/tui/app.rb +366 -0
- data/lib/gsd/tui/auto_complete.rb +79 -0
- data/lib/gsd/tui/colors.rb +111 -0
- data/lib/gsd/tui/command_palette.rb +126 -0
- data/lib/gsd/tui/header.rb +38 -0
- data/lib/gsd/tui/input_box.rb +199 -0
- data/lib/gsd/tui/spinner.rb +40 -0
- data/lib/gsd/tui/status_bar.rb +51 -0
- data/lib/gsd/tui.rb +17 -0
- data/lib/gsd/validator.rb +216 -0
- data/lib/gsd/verify.rb +175 -0
- data/lib/gsd/version.rb +5 -0
- data/lib/gsd/workstream.rb +91 -0
- metadata +231 -0
data/lib/gsd/ai/chat.rb
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'gsd/ai/context'
|
|
5
|
+
require 'gsd/ai/history'
|
|
6
|
+
require 'gsd/ai/streaming'
|
|
7
|
+
require 'gsd/ai/cost_tracker'
|
|
8
|
+
require 'gsd/ai/command_parser'
|
|
9
|
+
require 'gsd/ai/reference_resolver'
|
|
10
|
+
require 'gsd/ai/commands/help'
|
|
11
|
+
require 'gsd/ai/commands/clear'
|
|
12
|
+
require 'gsd/ai/commands/reset'
|
|
13
|
+
require 'gsd/ai/commands/model'
|
|
14
|
+
require 'gsd/ai/commands/cost'
|
|
15
|
+
require 'gsd/ai/commands/context'
|
|
16
|
+
require 'gsd/ai/commands/export'
|
|
17
|
+
require 'gsd/tools/registry'
|
|
18
|
+
require 'gsd/skills/loader'
|
|
19
|
+
require 'gsd/ai/providers/base'
|
|
20
|
+
require 'gsd/ai/providers/base'
|
|
21
|
+
require 'gsd/ai/providers/anthropic'
|
|
22
|
+
require 'gsd/ai/providers/openai'
|
|
23
|
+
require 'gsd/ai/providers/ollama'
|
|
24
|
+
require 'gsd/ai/providers/lmstudio'
|
|
25
|
+
require 'gsd/ai/providers/openrouter'
|
|
26
|
+
|
|
27
|
+
module Gsd
|
|
28
|
+
module AI
|
|
29
|
+
# Chat REPL Interface - Interface principal de conversa com IA
|
|
30
|
+
#
|
|
31
|
+
# Responsável por:
|
|
32
|
+
# - Gerenciar conversas com LLMs
|
|
33
|
+
# - Executar tool calls
|
|
34
|
+
# - Manter histórico de mensagens
|
|
35
|
+
# - Streaming de respostas
|
|
36
|
+
class Chat
|
|
37
|
+
attr_reader :context, :history, :tools, :skills, :provider
|
|
38
|
+
|
|
39
|
+
# Inicializa uma nova sessão de chat
|
|
40
|
+
#
|
|
41
|
+
# @param provider [Symbol] Provider de LLM (:anthropic, :openai, :ollama)
|
|
42
|
+
# @param model [String] Modelo do LLM
|
|
43
|
+
# @param cwd [String] Diretório de trabalho
|
|
44
|
+
# @param debug [Boolean] Habilitar modo debug
|
|
45
|
+
# @param cache_ttl [Integer] TTL do cache de contexto (segundos)
|
|
46
|
+
# @param persist_history [Boolean] Persistir histórico em disco
|
|
47
|
+
# @param budget [Float] Orçamento mensal em USD
|
|
48
|
+
def initialize(provider: :anthropic, model: nil, cwd: Dir.pwd, debug: false,
|
|
49
|
+
cache_ttl: 300, persist_history: true, budget: 100.0)
|
|
50
|
+
@cwd = cwd
|
|
51
|
+
@debug = debug
|
|
52
|
+
@provider = ProviderFactory.create(provider, model: model, debug: debug)
|
|
53
|
+
@context = Context.new(cwd: cwd, cache_ttl: cache_ttl)
|
|
54
|
+
persist_dir = persist_history ? File.join(cwd, '.gsd', 'ai') : nil
|
|
55
|
+
@history = History.new(persist_dir: persist_dir)
|
|
56
|
+
@tools = Tools::Registry.all
|
|
57
|
+
@skills = Skills::Loader.new(cwd: cwd).bundled_skills
|
|
58
|
+
@streaming = Streaming.new
|
|
59
|
+
@cost_tracker = CostTracker.new(budget: budget, persist_dir: persist_dir)
|
|
60
|
+
|
|
61
|
+
# Registra pricing do provider
|
|
62
|
+
register_provider_pricing
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Registra pricing do provider no cost tracker
|
|
66
|
+
#
|
|
67
|
+
# @return [void]
|
|
68
|
+
def register_provider_pricing
|
|
69
|
+
pricing = begin
|
|
70
|
+
@provider.class::MODELS
|
|
71
|
+
rescue
|
|
72
|
+
{}
|
|
73
|
+
end
|
|
74
|
+
provider_name = begin
|
|
75
|
+
@provider.class::PROVIDER_NAME
|
|
76
|
+
rescue
|
|
77
|
+
'unknown'
|
|
78
|
+
end
|
|
79
|
+
@cost_tracker.register_provider(provider_name, pricing) if pricing.any?
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Envia uma mensagem para a IA e retorna a resposta
|
|
83
|
+
#
|
|
84
|
+
# @param message [String] Mensagem do usuário
|
|
85
|
+
# @param options [Hash] Opções adicionais
|
|
86
|
+
# @return [String] Resposta da IA
|
|
87
|
+
def send(message, **options)
|
|
88
|
+
log_debug("User: #{message}")
|
|
89
|
+
|
|
90
|
+
# Preprocessar input (comandos e referências)
|
|
91
|
+
preprocessed = preprocess_input(message)
|
|
92
|
+
|
|
93
|
+
# Se for comando, executa e retorna
|
|
94
|
+
return preprocessed[:response] if preprocessed[:type] == :command
|
|
95
|
+
|
|
96
|
+
# Usa mensagem processada (expandida com referências)
|
|
97
|
+
final_message = preprocessed[:message]
|
|
98
|
+
|
|
99
|
+
# 1. Adicionar mensagem do usuário ao histórico
|
|
100
|
+
@history.add_user_message(final_message)
|
|
101
|
+
|
|
102
|
+
# 2. Carregar contexto do projeto
|
|
103
|
+
context_data = @context.load
|
|
104
|
+
|
|
105
|
+
# 3. Selecionar tools relevantes
|
|
106
|
+
available_tools = select_tools_for_message(message)
|
|
107
|
+
|
|
108
|
+
# 4. Construir system prompt
|
|
109
|
+
system_prompt = build_system_prompt(context_data)
|
|
110
|
+
|
|
111
|
+
# 5. Chamar LLM
|
|
112
|
+
response = call_llm(
|
|
113
|
+
system_prompt: system_prompt,
|
|
114
|
+
messages: @history.messages,
|
|
115
|
+
tools: available_tools,
|
|
116
|
+
stream: options.fetch(:stream, true)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# 6. Processar tool calls se existirem
|
|
120
|
+
if response.tool_calls
|
|
121
|
+
results = execute_tool_calls(response.tool_calls)
|
|
122
|
+
@history.add_tool_results(results)
|
|
123
|
+
|
|
124
|
+
# 7. Chamar LLM novamente com resultados das tools
|
|
125
|
+
response = call_llm(
|
|
126
|
+
system_prompt: system_prompt,
|
|
127
|
+
messages: @history.messages,
|
|
128
|
+
tools: available_tools,
|
|
129
|
+
stream: options.fetch(:stream, true)
|
|
130
|
+
)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# 8. Adicionar resposta ao histórico
|
|
134
|
+
@history.add_assistant_message(response.content)
|
|
135
|
+
|
|
136
|
+
# 9. Track custo
|
|
137
|
+
provider_name = begin
|
|
138
|
+
@provider.class::PROVIDER_NAME
|
|
139
|
+
rescue
|
|
140
|
+
'unknown'
|
|
141
|
+
end
|
|
142
|
+
@cost_tracker.track(
|
|
143
|
+
response.usage,
|
|
144
|
+
provider: provider_name,
|
|
145
|
+
model: @provider.model
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
log_debug("Assistant: #{response.content}")
|
|
149
|
+
log_debug("Cost: #{@cost_tracker.total_cost}")
|
|
150
|
+
log_debug("Tokens: #{@cost_tracker.total_tokens}")
|
|
151
|
+
log_debug("Budget used: #{@cost_tracker.stats[:budget_used_percent]}%")
|
|
152
|
+
|
|
153
|
+
response.content
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Inicia o REPL interativo
|
|
157
|
+
#
|
|
158
|
+
# @return [void]
|
|
159
|
+
def repl
|
|
160
|
+
puts "🤖 GSD AI Chat - #{provider.class} (#{@provider.model})"
|
|
161
|
+
puts "Digite 'quit' ou 'exit' para sair, 'clear' para limpar histórico"
|
|
162
|
+
puts "-" * 60
|
|
163
|
+
|
|
164
|
+
loop do
|
|
165
|
+
print "\n👤 "
|
|
166
|
+
input = $stdin.gets&.chomp
|
|
167
|
+
|
|
168
|
+
break if input.nil? || %w[quit exit].include?(input.downcase)
|
|
169
|
+
|
|
170
|
+
if input.downcase == 'clear'
|
|
171
|
+
@history.clear
|
|
172
|
+
puts "🧹 Histórico limpo!"
|
|
173
|
+
next
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
next if input.strip.empty?
|
|
177
|
+
|
|
178
|
+
begin
|
|
179
|
+
response = send(input, stream: true)
|
|
180
|
+
puts "\n🤖 #{response}"
|
|
181
|
+
rescue => e
|
|
182
|
+
puts "\n❌ Erro: #{e.message}"
|
|
183
|
+
log_debug(e.backtrace.join("\n")) if @debug
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
puts "\n👋 Até logo!"
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Limpa o histórico de conversas
|
|
191
|
+
#
|
|
192
|
+
# @return [void]
|
|
193
|
+
def clear_history
|
|
194
|
+
@history.clear
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Retorna estatísticas de uso
|
|
198
|
+
#
|
|
199
|
+
# @return [Hash] Estatísticas de tokens e custo
|
|
200
|
+
def stats
|
|
201
|
+
{
|
|
202
|
+
messages_count: @history.messages.count,
|
|
203
|
+
total_tokens: @cost_tracker.total_tokens,
|
|
204
|
+
total_cost: @cost_tracker.total_cost,
|
|
205
|
+
context_size: @context.size,
|
|
206
|
+
cache_info: @context.cache_info,
|
|
207
|
+
history_stats: @history.stats,
|
|
208
|
+
cost_stats: @cost_tracker.stats,
|
|
209
|
+
budget_remaining: @cost_tracker.stats[:budget_remaining],
|
|
210
|
+
budget_used_percent: @cost_tracker.stats[:budget_used_percent]
|
|
211
|
+
}
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Retorna informações detalhadas de custo
|
|
215
|
+
#
|
|
216
|
+
# @return [Hash] Informações de custo
|
|
217
|
+
def cost_info
|
|
218
|
+
@cost_tracker.stats
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Retorna histórico de custos
|
|
222
|
+
#
|
|
223
|
+
# @param limit [Integer] Limite de registros
|
|
224
|
+
# @return [Array] Histórico
|
|
225
|
+
def cost_history(limit: 50)
|
|
226
|
+
@cost_tracker.history(limit: limit)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Define orçamento
|
|
230
|
+
#
|
|
231
|
+
# @param budget [Float] Orçamento em USD
|
|
232
|
+
# @return [void]
|
|
233
|
+
def budget=(budget)
|
|
234
|
+
@cost_tracker.budget = budget
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Retorna orçamento restante
|
|
238
|
+
#
|
|
239
|
+
# @return [Float] Orçamento restante
|
|
240
|
+
def budget_remaining
|
|
241
|
+
@cost_tracker.stats[:budget_remaining]
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Reseta estatísticas de custo
|
|
245
|
+
#
|
|
246
|
+
# @return [void]
|
|
247
|
+
def reset_cost_stats
|
|
248
|
+
@cost_tracker.reset
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Retorna informações do cache de contexto
|
|
252
|
+
#
|
|
253
|
+
# @return [Hash] Informações do cache
|
|
254
|
+
def cache_info
|
|
255
|
+
@context.cache_info
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Retorna informações do histórico
|
|
259
|
+
#
|
|
260
|
+
# @return [Hash] Estatísticas do histórico
|
|
261
|
+
def history_info
|
|
262
|
+
@history.stats
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Recarrega o contexto
|
|
266
|
+
#
|
|
267
|
+
# @return [Hash] Novo contexto
|
|
268
|
+
def reload_context
|
|
269
|
+
@context.reload
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Limpa o cache de contexto
|
|
273
|
+
#
|
|
274
|
+
# @return [void]
|
|
275
|
+
def clear_cache
|
|
276
|
+
@context.clear_cache
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
private
|
|
280
|
+
|
|
281
|
+
# Preprocessa input do usuário (comandos e referências)
|
|
282
|
+
#
|
|
283
|
+
# @param message [String] Input original
|
|
284
|
+
# @return [Hash] { type: :message|:command, message: String, response: String }
|
|
285
|
+
def preprocess_input(message)
|
|
286
|
+
parsed = CommandParser.parse(message)
|
|
287
|
+
|
|
288
|
+
case parsed.type
|
|
289
|
+
when :command
|
|
290
|
+
{ type: :command, response: execute_command(parsed.command, parsed.args) }
|
|
291
|
+
when :reference
|
|
292
|
+
resolved = resolve_references(parsed.references)
|
|
293
|
+
expanded = expand_message_with_references(message, resolved)
|
|
294
|
+
{ type: :message, message: expanded }
|
|
295
|
+
else
|
|
296
|
+
{ type: :message, message: message }
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Executa um comando /
|
|
301
|
+
#
|
|
302
|
+
# @param command [String] Nome do comando
|
|
303
|
+
# @param args [Array<String>] Argumentos
|
|
304
|
+
# @return [String] Resposta do comando
|
|
305
|
+
def execute_command(command, args)
|
|
306
|
+
cmd_class = case command
|
|
307
|
+
when 'help' then Commands::Help
|
|
308
|
+
when 'clear' then Commands::Clear
|
|
309
|
+
when 'reset' then Commands::Reset
|
|
310
|
+
when 'model' then Commands::Model
|
|
311
|
+
when 'cost' then Commands::Cost
|
|
312
|
+
when 'context' then Commands::Context
|
|
313
|
+
when 'export' then Commands::Export
|
|
314
|
+
else nil
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
return "Comando desconhecido: /#{command}\nUse /help para listar comandos." if cmd_class.nil?
|
|
318
|
+
|
|
319
|
+
cmd = cmd_class.new(self, args)
|
|
320
|
+
cmd.execute
|
|
321
|
+
rescue => e
|
|
322
|
+
"❌ Erro ao executar /#{command}: #{e.message}"
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# Resolve referências @ para conteúdo
|
|
326
|
+
#
|
|
327
|
+
# @param references [Array<String>] Lista de referências
|
|
328
|
+
# @return [Hash] Resultado do resolver
|
|
329
|
+
def resolve_references(references)
|
|
330
|
+
resolver = ReferenceResolver.new(cwd: @cwd, provider: @provider)
|
|
331
|
+
resolver.resolve(references)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Expande mensagem com conteúdo das referências
|
|
335
|
+
#
|
|
336
|
+
# @param original [String] Mensagem original
|
|
337
|
+
# @param resolved [Hash] Resultado do resolver
|
|
338
|
+
# @return [String] Mensagem expandida
|
|
339
|
+
def expand_message_with_references(original, resolved)
|
|
340
|
+
return original if resolved[:text].nil? || resolved[:text].empty?
|
|
341
|
+
|
|
342
|
+
<<~MESSAGE
|
|
343
|
+
#{original}
|
|
344
|
+
|
|
345
|
+
[Referências carregadas:]
|
|
346
|
+
#{resolved[:text]}
|
|
347
|
+
MESSAGE
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# Seleciona tools relevantes para a mensagem
|
|
351
|
+
#
|
|
352
|
+
# @param message [String] Mensagem do usuário
|
|
353
|
+
# @return [Array<Tool>] Tools relevantes
|
|
354
|
+
def select_tools_for_message(message)
|
|
355
|
+
# Por enquanto, retorna todas as tools
|
|
356
|
+
# No futuro, pode usar embedding ou heurísticas para filtrar
|
|
357
|
+
@tools
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# Constrói o system prompt com contexto do projeto
|
|
361
|
+
#
|
|
362
|
+
# @param context_data [Hash] Dados de contexto
|
|
363
|
+
# @return [String] System prompt
|
|
364
|
+
def build_system_prompt(context_data)
|
|
365
|
+
<<~PROMPT
|
|
366
|
+
Você é o GSD AI, um assistente especializado em gerenciamento de projetos GSD (Get Shit Done).
|
|
367
|
+
|
|
368
|
+
## Contexto do Projeto
|
|
369
|
+
|
|
370
|
+
### Estado Atual
|
|
371
|
+
#{JSON.pretty_generate(context_data[:state])}
|
|
372
|
+
|
|
373
|
+
### Phase Atual
|
|
374
|
+
#{context_data[:current_phase]&.dig('name') || 'N/A'}
|
|
375
|
+
|
|
376
|
+
### Roadmap
|
|
377
|
+
#{context_data[:roadmap]&.dig('summary') || 'N/A'}
|
|
378
|
+
|
|
379
|
+
## Suas Capacidades
|
|
380
|
+
|
|
381
|
+
Você tem acesso às seguintes ferramentas:
|
|
382
|
+
- Bash: Executar comandos shell
|
|
383
|
+
- File operations: Ler, escrever e editar arquivos
|
|
384
|
+
- Grep/Glob: Buscar no código
|
|
385
|
+
- GSD commands: Operações de state, phase, roadmap
|
|
386
|
+
- Tasks: Gerenciar tarefas
|
|
387
|
+
- Web: Pesquisar e fetch de URLs
|
|
388
|
+
|
|
389
|
+
## Instruções
|
|
390
|
+
|
|
391
|
+
1. Seja direto e prático
|
|
392
|
+
2. Use ferramentas quando necessário
|
|
393
|
+
3. Mantenha o foco nas tarefas do usuário
|
|
394
|
+
4. Reporte erros claramente
|
|
395
|
+
5. Sugira próximos passos quando relevante
|
|
396
|
+
|
|
397
|
+
## Formato de Resposta
|
|
398
|
+
|
|
399
|
+
- Use Markdown para formatação
|
|
400
|
+
- Inclua código em blocks quando relevante
|
|
401
|
+
- Seja conciso mas completo
|
|
402
|
+
PROMPT
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# Chama o provedor de LLM
|
|
406
|
+
#
|
|
407
|
+
# @param system_prompt [String] System prompt
|
|
408
|
+
# @param messages [Array] Histórico de mensagens
|
|
409
|
+
# @param tools [Array] Tools disponíveis
|
|
410
|
+
# @param stream [Boolean] Habilitar streaming
|
|
411
|
+
# @return [OpenStruct] Resposta com content, tool_calls, usage
|
|
412
|
+
def call_llm(system_prompt:, messages:, tools:, stream:)
|
|
413
|
+
@provider.call(
|
|
414
|
+
system_prompt: system_prompt,
|
|
415
|
+
messages: messages,
|
|
416
|
+
tools: tools,
|
|
417
|
+
stream: stream
|
|
418
|
+
) do |chunk|
|
|
419
|
+
@streaming.process(chunk) if stream
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
# Executa tool calls retornados pelo LLM
|
|
424
|
+
#
|
|
425
|
+
# @param tool_calls [Array] Lista de tool calls
|
|
426
|
+
# @return [Array] Resultados das execuções
|
|
427
|
+
def execute_tool_calls(tool_calls)
|
|
428
|
+
tool_calls.map do |call|
|
|
429
|
+
tool = @tools.find { |t| t.name == call.name }
|
|
430
|
+
|
|
431
|
+
if tool
|
|
432
|
+
begin
|
|
433
|
+
result = tool.execute(call.arguments, cwd: @cwd)
|
|
434
|
+
{
|
|
435
|
+
tool_call_id: call.id,
|
|
436
|
+
success: true,
|
|
437
|
+
result: result
|
|
438
|
+
}
|
|
439
|
+
rescue => e
|
|
440
|
+
{
|
|
441
|
+
tool_call_id: call.id,
|
|
442
|
+
success: false,
|
|
443
|
+
error: e.message
|
|
444
|
+
}
|
|
445
|
+
end
|
|
446
|
+
else
|
|
447
|
+
{
|
|
448
|
+
tool_call_id: call.id,
|
|
449
|
+
success: false,
|
|
450
|
+
error: "Tool not found: #{call.name}"
|
|
451
|
+
}
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
# Log de debug
|
|
457
|
+
#
|
|
458
|
+
# @param message [String] Mensagem de log
|
|
459
|
+
def log_debug(message)
|
|
460
|
+
return unless @debug
|
|
461
|
+
|
|
462
|
+
puts "[DEBUG] #{message}"
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
# Factory para criar provedores de LLM
|
|
467
|
+
module ProviderFactory
|
|
468
|
+
def self.create(provider, model: nil, debug: false, **options)
|
|
469
|
+
case provider
|
|
470
|
+
when :anthropic
|
|
471
|
+
Providers::Anthropic.new(model: model, debug: debug)
|
|
472
|
+
when :openai
|
|
473
|
+
Providers::OpenAI.new(model: model, debug: debug)
|
|
474
|
+
when :ollama
|
|
475
|
+
Providers::Ollama.new(model: model, debug: debug)
|
|
476
|
+
when :lmstudio
|
|
477
|
+
Providers::LMStudio.new(model: model, debug: debug, **options)
|
|
478
|
+
when :openrouter
|
|
479
|
+
Providers::OpenRouter.new(model: model, debug: debug, **options)
|
|
480
|
+
else
|
|
481
|
+
raise ArgumentError, "Unknown provider: #{provider}. Available: :anthropic, :openai, :ollama, :lmstudio, :openrouter"
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
end
|