aircana 3.0.0.rc8 → 3.1.0
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 +4 -4
- data/.rspec_status +187 -187
- data/.rubocop.yml +6 -0
- data/CHANGELOG.md +62 -14
- data/CLAUDE.md +166 -34
- data/README.md +252 -154
- data/lib/aircana/cli/app.rb +6 -1
- data/lib/aircana/cli/commands/agents.rb +237 -23
- data/lib/aircana/cli/commands/generate.rb +11 -0
- data/lib/aircana/configuration.rb +19 -0
- data/lib/aircana/contexts/confluence.rb +12 -11
- data/lib/aircana/contexts/confluence_content.rb +3 -2
- data/lib/aircana/contexts/local.rb +5 -5
- data/lib/aircana/contexts/manifest.rb +31 -7
- data/lib/aircana/contexts/web.rb +13 -11
- data/lib/aircana/generators/agents_generator.rb +10 -4
- data/lib/aircana/templates/hooks/post_tool_use.erb +0 -6
- data/lib/aircana/version.rb +1 -1
- data/{agents → spec_target_1760656566_428/agents}/test-agent/manifest.json +1 -0
- data/spec_target_1760656588_38/agents/test-agent/manifest.json +16 -0
- data/spec_target_1760656647_612/agents/test-agent/manifest.json +16 -0
- data/spec_target_1760656660_113/agents/test-agent/manifest.json +16 -0
- data/spec_target_1760656689_268/agents/test-agent/manifest.json +16 -0
- data/spec_target_1760656710_387/agents/test-agent/manifest.json +16 -0
- metadata +14 -13
- data/agents/apply_feedback.md +0 -92
- data/agents/executor.md +0 -85
- data/agents/jira.md +0 -46
- data/agents/planner.md +0 -64
- data/agents/reviewer.md +0 -95
- data/agents/sub-agent-coordinator.md +0 -91
@@ -15,12 +15,20 @@ module Aircana
|
|
15
15
|
class << self # rubocop:disable Metrics/ClassLength
|
16
16
|
def refresh(agent)
|
17
17
|
normalized_agent = normalize_string(agent)
|
18
|
+
|
19
|
+
# Check if this is a local knowledge base agent
|
20
|
+
kb_type = Aircana::Contexts::Manifest.kb_type_from_manifest(normalized_agent)
|
21
|
+
if kb_type == "local"
|
22
|
+
Aircana.human_logger.info "⊘ Skipping #{normalized_agent} (local knowledge base - no refresh needed)"
|
23
|
+
return
|
24
|
+
end
|
25
|
+
|
18
26
|
perform_manifest_aware_refresh(normalized_agent)
|
19
27
|
rescue Aircana::Error => e
|
20
28
|
handle_refresh_error(normalized_agent, e)
|
21
29
|
end
|
22
30
|
|
23
|
-
def create
|
31
|
+
def create
|
24
32
|
prompt = TTY::Prompt.new
|
25
33
|
|
26
34
|
agent_name = prompt.ask("Agent name:")
|
@@ -28,6 +36,20 @@ module Aircana
|
|
28
36
|
model = prompt.select("Select a model for your agent:", SUPPORTED_CLAUDE_MODELS)
|
29
37
|
color = prompt.select("Select a color for your agent:", SUPPORTED_CLAUDE_COLORS)
|
30
38
|
|
39
|
+
# Prompt for knowledge base type
|
40
|
+
kb_type = prompt.select("Knowledge base type:", [
|
41
|
+
{
|
42
|
+
name: "Remote - Stored in ~/.claude/agents/, not version controlled, " \
|
43
|
+
"requires refresh",
|
44
|
+
value: "remote"
|
45
|
+
},
|
46
|
+
{
|
47
|
+
name: "Local - Stored in agents/<name>/knowledge/, version controlled, " \
|
48
|
+
"no refresh needed",
|
49
|
+
value: "local"
|
50
|
+
}
|
51
|
+
])
|
52
|
+
|
31
53
|
description = description_from_claude(short_description)
|
32
54
|
normalized_agent_name = normalize_string(agent_name)
|
33
55
|
|
@@ -36,16 +58,17 @@ module Aircana
|
|
36
58
|
description:,
|
37
59
|
short_description:,
|
38
60
|
model: normalize_string(model),
|
39
|
-
color: normalize_string(color)
|
61
|
+
color: normalize_string(color),
|
62
|
+
kb_type:
|
40
63
|
).generate
|
41
64
|
|
42
65
|
Aircana.human_logger.success "Agent created at #{file}"
|
43
66
|
|
44
67
|
# Prompt for knowledge fetching
|
45
|
-
prompt_for_knowledge_fetch(prompt, normalized_agent_name)
|
68
|
+
prompt_for_knowledge_fetch(prompt, normalized_agent_name, kb_type)
|
46
69
|
|
47
70
|
# Prompt for web URL fetching
|
48
|
-
prompt_for_url_fetch(prompt, normalized_agent_name)
|
71
|
+
prompt_for_url_fetch(prompt, normalized_agent_name, kb_type)
|
49
72
|
|
50
73
|
# Prompt for agent file review
|
51
74
|
prompt_for_agent_review(prompt, file)
|
@@ -63,7 +86,7 @@ module Aircana
|
|
63
86
|
print_agents_list(agent_folders)
|
64
87
|
end
|
65
88
|
|
66
|
-
def add_url(agent, url)
|
89
|
+
def add_url(agent, url)
|
67
90
|
normalized_agent = normalize_string(agent)
|
68
91
|
|
69
92
|
unless agent_exists?(normalized_agent)
|
@@ -71,8 +94,11 @@ module Aircana
|
|
71
94
|
exit 1
|
72
95
|
end
|
73
96
|
|
97
|
+
# Get kb_type from manifest to know where to store
|
98
|
+
kb_type = Aircana::Contexts::Manifest.kb_type_from_manifest(normalized_agent)
|
99
|
+
|
74
100
|
web = Aircana::Contexts::Web.new
|
75
|
-
result = web.fetch_url_for(agent: normalized_agent, url: url)
|
101
|
+
result = web.fetch_url_for(agent: normalized_agent, url: url, kb_type: kb_type)
|
76
102
|
|
77
103
|
if result
|
78
104
|
# Update manifest with the new URL
|
@@ -101,7 +127,7 @@ module Aircana
|
|
101
127
|
exit 1
|
102
128
|
end
|
103
129
|
|
104
|
-
def refresh_all
|
130
|
+
def refresh_all
|
105
131
|
agent_names = all_agents
|
106
132
|
|
107
133
|
if agent_names.empty?
|
@@ -115,11 +141,22 @@ module Aircana
|
|
115
141
|
total: agent_names.size,
|
116
142
|
successful: 0,
|
117
143
|
failed: 0,
|
144
|
+
skipped: 0,
|
118
145
|
total_pages: 0,
|
119
|
-
failed_agents: []
|
146
|
+
failed_agents: [],
|
147
|
+
skipped_agents: []
|
120
148
|
}
|
121
149
|
|
122
150
|
agent_names.each do |agent_name|
|
151
|
+
# Check if this is a local knowledge base agent
|
152
|
+
kb_type = Aircana::Contexts::Manifest.kb_type_from_manifest(agent_name)
|
153
|
+
if kb_type == "local"
|
154
|
+
Aircana.human_logger.info "⊘ Skipping #{agent_name} (local knowledge base)"
|
155
|
+
results[:skipped] += 1
|
156
|
+
results[:skipped_agents] << agent_name
|
157
|
+
next
|
158
|
+
end
|
159
|
+
|
123
160
|
result = refresh_single_agent(agent_name)
|
124
161
|
if result[:success]
|
125
162
|
results[:successful] += 1
|
@@ -133,11 +170,56 @@ module Aircana
|
|
133
170
|
print_refresh_all_summary(results)
|
134
171
|
end
|
135
172
|
|
173
|
+
def migrate_to_local
|
174
|
+
agent_names = all_agents
|
175
|
+
|
176
|
+
if agent_names.empty?
|
177
|
+
Aircana.human_logger.info "No agents found to migrate."
|
178
|
+
return
|
179
|
+
end
|
180
|
+
|
181
|
+
# Filter to only remote agents
|
182
|
+
remote_agents = agent_names.select do |agent_name|
|
183
|
+
Aircana::Contexts::Manifest.kb_type_from_manifest(agent_name) == "remote"
|
184
|
+
end
|
185
|
+
|
186
|
+
if remote_agents.empty?
|
187
|
+
Aircana.human_logger.info "No remote agents found. All agents are already using local knowledge bases."
|
188
|
+
return
|
189
|
+
end
|
190
|
+
|
191
|
+
Aircana.human_logger.warn "⚠️ This will migrate #{remote_agents.size} agent(s) to local knowledge bases."
|
192
|
+
Aircana.human_logger.warn "Knowledge will be version-controlled and stored in " \
|
193
|
+
".claude/agents/<agent>/knowledge/"
|
194
|
+
Aircana.human_logger.info ""
|
195
|
+
|
196
|
+
results = {
|
197
|
+
total: remote_agents.size,
|
198
|
+
successful: 0,
|
199
|
+
failed: 0,
|
200
|
+
migrated_agents: [],
|
201
|
+
failed_agents: []
|
202
|
+
}
|
203
|
+
|
204
|
+
remote_agents.each do |agent_name|
|
205
|
+
result = migrate_single_agent(agent_name)
|
206
|
+
if result[:success]
|
207
|
+
results[:successful] += 1
|
208
|
+
results[:migrated_agents] << agent_name
|
209
|
+
else
|
210
|
+
results[:failed] += 1
|
211
|
+
results[:failed_agents] << { name: agent_name, error: result[:error] }
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
print_migration_summary(results)
|
216
|
+
end
|
217
|
+
|
136
218
|
private
|
137
219
|
|
138
|
-
def perform_refresh(normalized_agent)
|
220
|
+
def perform_refresh(normalized_agent, kb_type = "remote")
|
139
221
|
confluence = Aircana::Contexts::Confluence.new
|
140
|
-
result = confluence.fetch_pages_for(agent: normalized_agent)
|
222
|
+
result = confluence.fetch_pages_for(agent: normalized_agent, kb_type: kb_type)
|
141
223
|
|
142
224
|
log_refresh_result(normalized_agent, result[:pages_count])
|
143
225
|
result
|
@@ -151,7 +233,7 @@ module Aircana
|
|
151
233
|
end
|
152
234
|
end
|
153
235
|
|
154
|
-
def perform_manifest_aware_refresh(normalized_agent)
|
236
|
+
def perform_manifest_aware_refresh(normalized_agent)
|
155
237
|
total_pages = 0
|
156
238
|
all_sources = []
|
157
239
|
|
@@ -185,7 +267,10 @@ module Aircana
|
|
185
267
|
{ pages_count: total_pages, sources: all_sources }
|
186
268
|
end
|
187
269
|
|
188
|
-
def ensure_gitignore_entry
|
270
|
+
def ensure_gitignore_entry(kb_type = "remote")
|
271
|
+
# Only add to gitignore for remote knowledge bases
|
272
|
+
return if kb_type == "local"
|
273
|
+
|
189
274
|
gitignore_path = gitignore_file_path
|
190
275
|
pattern = gitignore_pattern
|
191
276
|
|
@@ -263,24 +348,34 @@ module Aircana
|
|
263
348
|
PROMPT
|
264
349
|
end
|
265
350
|
|
266
|
-
def prompt_for_knowledge_fetch(prompt, normalized_agent_name)
|
351
|
+
def prompt_for_knowledge_fetch(prompt, normalized_agent_name, kb_type)
|
267
352
|
return unless confluence_configured?
|
268
353
|
|
269
354
|
if prompt.yes?("Would you like to fetch knowledge for this agent from Confluence now?")
|
270
355
|
Aircana.human_logger.info "Fetching knowledge from Confluence..."
|
271
|
-
result = perform_refresh(normalized_agent_name)
|
272
|
-
ensure_gitignore_entry if result[:pages_count]&.positive?
|
356
|
+
result = perform_refresh(normalized_agent_name, kb_type)
|
357
|
+
ensure_gitignore_entry(kb_type) if result[:pages_count]&.positive?
|
273
358
|
else
|
359
|
+
refresh_message = if kb_type == "local"
|
360
|
+
"fetch knowledge"
|
361
|
+
else
|
362
|
+
"run 'aircana agents refresh #{normalized_agent_name}'"
|
363
|
+
end
|
274
364
|
Aircana.human_logger.info(
|
275
|
-
"Skipping knowledge fetch. You can
|
365
|
+
"Skipping knowledge fetch. You can #{refresh_message} later."
|
276
366
|
)
|
277
367
|
end
|
278
368
|
rescue Aircana::Error => e
|
279
369
|
Aircana.human_logger.warn "Failed to fetch knowledge: #{e.message}"
|
280
|
-
|
370
|
+
refresh_message = if kb_type == "local"
|
371
|
+
"fetch knowledge"
|
372
|
+
else
|
373
|
+
"try again later with 'aircana agents refresh #{normalized_agent_name}'"
|
374
|
+
end
|
375
|
+
Aircana.human_logger.info "You can #{refresh_message}"
|
281
376
|
end
|
282
377
|
|
283
|
-
def prompt_for_url_fetch(prompt, normalized_agent_name
|
378
|
+
def prompt_for_url_fetch(prompt, normalized_agent_name, kb_type)
|
284
379
|
return unless prompt.yes?("Would you like to add web URLs for this agent's knowledge base?")
|
285
380
|
|
286
381
|
urls = []
|
@@ -301,11 +396,11 @@ module Aircana
|
|
301
396
|
begin
|
302
397
|
Aircana.human_logger.info "Fetching #{urls.size} URL(s)..."
|
303
398
|
web = Aircana::Contexts::Web.new
|
304
|
-
result = web.fetch_urls_for(agent: normalized_agent_name, urls: urls)
|
399
|
+
result = web.fetch_urls_for(agent: normalized_agent_name, urls: urls, kb_type: kb_type)
|
305
400
|
|
306
401
|
if result[:pages_count].positive?
|
307
402
|
Aircana.human_logger.success "Successfully fetched #{result[:pages_count]} URL(s)"
|
308
|
-
ensure_gitignore_entry
|
403
|
+
ensure_gitignore_entry(kb_type)
|
309
404
|
else
|
310
405
|
Aircana.human_logger.warn "No URLs were successfully fetched"
|
311
406
|
end
|
@@ -325,7 +420,7 @@ module Aircana
|
|
325
420
|
open_file_in_editor(file_path)
|
326
421
|
end
|
327
422
|
|
328
|
-
def confluence_configured?
|
423
|
+
def confluence_configured?
|
329
424
|
config = Aircana.configuration
|
330
425
|
|
331
426
|
base_url_present = !config.confluence_base_url.nil? && !config.confluence_base_url.empty?
|
@@ -401,7 +496,7 @@ module Aircana
|
|
401
496
|
find_agent_folders(agent_dir)
|
402
497
|
end
|
403
498
|
|
404
|
-
def refresh_single_agent(agent_name)
|
499
|
+
def refresh_single_agent(agent_name)
|
405
500
|
Aircana.human_logger.info "Refreshing agent '#{agent_name}'..."
|
406
501
|
|
407
502
|
begin
|
@@ -422,12 +517,125 @@ module Aircana
|
|
422
517
|
end
|
423
518
|
end
|
424
519
|
|
425
|
-
def print_refresh_all_summary(results)
|
520
|
+
def print_refresh_all_summary(results)
|
426
521
|
Aircana.human_logger.info ""
|
427
522
|
Aircana.human_logger.info "=== Refresh All Summary ==="
|
428
523
|
Aircana.human_logger.success "✓ Successful: #{results[:successful]}/#{results[:total]} agents"
|
429
524
|
Aircana.human_logger.success "✓ Total pages refreshed: #{results[:total_pages]}"
|
430
525
|
|
526
|
+
if results[:skipped].positive?
|
527
|
+
Aircana.human_logger.info "⊘ Skipped: #{results[:skipped]} agent(s) (local knowledge base)"
|
528
|
+
end
|
529
|
+
|
530
|
+
if results[:failed].positive?
|
531
|
+
Aircana.human_logger.error "✗ Failed: #{results[:failed]} agents"
|
532
|
+
Aircana.human_logger.info ""
|
533
|
+
Aircana.human_logger.info "Failed agents:"
|
534
|
+
results[:failed_agents].each do |failed_agent|
|
535
|
+
Aircana.human_logger.error " - #{failed_agent[:name]}: #{failed_agent[:error]}"
|
536
|
+
end
|
537
|
+
end
|
538
|
+
|
539
|
+
Aircana.human_logger.info ""
|
540
|
+
end
|
541
|
+
|
542
|
+
def migrate_single_agent(agent_name)
|
543
|
+
Aircana.human_logger.info "Migrating agent '#{agent_name}'..."
|
544
|
+
|
545
|
+
begin
|
546
|
+
# Step 1: Refresh knowledge from sources to ensure we have latest
|
547
|
+
Aircana.human_logger.info " Refreshing knowledge from sources..."
|
548
|
+
perform_manifest_aware_refresh(agent_name)
|
549
|
+
|
550
|
+
# Step 2: Copy knowledge files from global to local directory
|
551
|
+
Aircana.human_logger.info " Copying knowledge files to local directory..."
|
552
|
+
copy_knowledge_files(agent_name)
|
553
|
+
|
554
|
+
# Step 3: Update manifest kb_type to "local"
|
555
|
+
Aircana.human_logger.info " Updating manifest..."
|
556
|
+
sources = Aircana::Contexts::Manifest.sources_from_manifest(agent_name)
|
557
|
+
Aircana::Contexts::Manifest.update_manifest(agent_name, sources, kb_type: "local")
|
558
|
+
|
559
|
+
# Step 4: Regenerate agent file with new kb_type
|
560
|
+
Aircana.human_logger.info " Regenerating agent file..."
|
561
|
+
regenerate_agent_file(agent_name)
|
562
|
+
|
563
|
+
Aircana.human_logger.success "✓ Successfully migrated '#{agent_name}'"
|
564
|
+
{ success: true }
|
565
|
+
rescue StandardError => e
|
566
|
+
Aircana.human_logger.error "✗ Failed to migrate agent '#{agent_name}': #{e.message}"
|
567
|
+
{ success: false, error: e.message }
|
568
|
+
end
|
569
|
+
end
|
570
|
+
|
571
|
+
def copy_knowledge_files(agent_name)
|
572
|
+
config = Aircana.configuration
|
573
|
+
source_dir = config.global_agent_knowledge_path(agent_name)
|
574
|
+
dest_dir = config.local_agent_knowledge_path(agent_name)
|
575
|
+
|
576
|
+
unless Dir.exist?(source_dir)
|
577
|
+
Aircana.human_logger.warn " No knowledge files found at #{source_dir}"
|
578
|
+
return
|
579
|
+
end
|
580
|
+
|
581
|
+
FileUtils.mkdir_p(dest_dir)
|
582
|
+
|
583
|
+
# Copy all markdown files
|
584
|
+
Dir.glob(File.join(source_dir, "*.md")).each do |file|
|
585
|
+
filename = File.basename(file)
|
586
|
+
dest_file = File.join(dest_dir, filename)
|
587
|
+
FileUtils.cp(file, dest_file)
|
588
|
+
Aircana.human_logger.info " Copied #{filename}"
|
589
|
+
end
|
590
|
+
end
|
591
|
+
|
592
|
+
def regenerate_agent_file(agent_name)
|
593
|
+
# Read existing agent file to get metadata
|
594
|
+
agent_file_path = File.join(Aircana.configuration.agents_dir, "#{agent_name}.md")
|
595
|
+
unless File.exist?(agent_file_path)
|
596
|
+
Aircana.human_logger.warn " Agent file not found at #{agent_file_path}, skipping regeneration"
|
597
|
+
return
|
598
|
+
end
|
599
|
+
|
600
|
+
# Parse frontmatter to get agent metadata
|
601
|
+
content = File.read(agent_file_path)
|
602
|
+
frontmatter_match = content.match(/^---\n(.*?)\n---\n(.*)$/m)
|
603
|
+
return unless frontmatter_match
|
604
|
+
|
605
|
+
frontmatter = frontmatter_match[1]
|
606
|
+
description = frontmatter_match[2]
|
607
|
+
|
608
|
+
# Parse YAML-like frontmatter
|
609
|
+
metadata = {}
|
610
|
+
frontmatter.lines.each do |line|
|
611
|
+
key, value = line.strip.split(":", 2)
|
612
|
+
metadata[key.strip] = value&.strip
|
613
|
+
end
|
614
|
+
|
615
|
+
# Generate new agent file with kb_type="local"
|
616
|
+
Generators::AgentsGenerator.new(
|
617
|
+
agent_name: agent_name,
|
618
|
+
description: description,
|
619
|
+
short_description: metadata["description"],
|
620
|
+
model: metadata["model"],
|
621
|
+
color: metadata["color"],
|
622
|
+
kb_type: "local"
|
623
|
+
).generate
|
624
|
+
end
|
625
|
+
|
626
|
+
def print_migration_summary(results)
|
627
|
+
Aircana.human_logger.info ""
|
628
|
+
Aircana.human_logger.info "=== Migration Summary ==="
|
629
|
+
Aircana.human_logger.success "✓ Successfully migrated: #{results[:successful]}/#{results[:total]} agents"
|
630
|
+
|
631
|
+
if results[:successful].positive?
|
632
|
+
Aircana.human_logger.info ""
|
633
|
+
Aircana.human_logger.info "Migrated agents:"
|
634
|
+
results[:migrated_agents].each do |agent_name|
|
635
|
+
Aircana.human_logger.success " ✓ #{agent_name}"
|
636
|
+
end
|
637
|
+
end
|
638
|
+
|
431
639
|
if results[:failed].positive?
|
432
640
|
Aircana.human_logger.error "✗ Failed: #{results[:failed]} agents"
|
433
641
|
Aircana.human_logger.info ""
|
@@ -437,6 +645,12 @@ module Aircana
|
|
437
645
|
end
|
438
646
|
end
|
439
647
|
|
648
|
+
if results[:successful].positive?
|
649
|
+
Aircana.human_logger.info ""
|
650
|
+
Aircana.human_logger.warn "⚠️ Knowledge is now version-controlled in .claude/agents/<agent>/knowledge/"
|
651
|
+
Aircana.human_logger.info "Review and commit these changes to your repository."
|
652
|
+
end
|
653
|
+
|
440
654
|
Aircana.human_logger.info ""
|
441
655
|
end
|
442
656
|
end
|
@@ -25,6 +25,7 @@ module Aircana
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def run
|
28
|
+
clean_output_directories
|
28
29
|
generators.each(&:generate)
|
29
30
|
generate_default_agents
|
30
31
|
generate_default_hooks
|
@@ -33,6 +34,16 @@ module Aircana
|
|
33
34
|
|
34
35
|
private
|
35
36
|
|
37
|
+
def clean_output_directories
|
38
|
+
# Remove stale command files to prevent duplicates during init
|
39
|
+
commands_dir = File.join(Aircana.configuration.output_dir, "commands")
|
40
|
+
FileUtils.rm_rf(Dir.glob("#{commands_dir}/*")) if Dir.exist?(commands_dir)
|
41
|
+
|
42
|
+
# Remove stale agent files for consistency
|
43
|
+
agents_dir = File.join(Aircana.configuration.output_dir, "agents")
|
44
|
+
FileUtils.rm_rf(Dir.glob("#{agents_dir}/*")) if Dir.exist?(agents_dir)
|
45
|
+
end
|
46
|
+
|
36
47
|
def generate_default_agents
|
37
48
|
Aircana::Generators::AgentsGenerator.available_default_agents.each do |agent_name|
|
38
49
|
Aircana::Generators::AgentsGenerator.create_default_agent(agent_name)
|
@@ -54,6 +54,25 @@ module Aircana
|
|
54
54
|
File.join(@global_agents_dir, "#{plugin_name}-#{agent_name}", "knowledge")
|
55
55
|
end
|
56
56
|
|
57
|
+
# Returns the local knowledge directory path for an agent
|
58
|
+
# Format: <plugin-root>/agents/<agent-name>/knowledge/
|
59
|
+
def local_agent_knowledge_path(agent_name)
|
60
|
+
File.join(@agents_dir, agent_name, "knowledge")
|
61
|
+
end
|
62
|
+
|
63
|
+
# Returns the appropriate knowledge directory path based on kb_type
|
64
|
+
# kb_type can be "remote" or "local"
|
65
|
+
def agent_knowledge_path(agent_name, kb_type)
|
66
|
+
case kb_type
|
67
|
+
when "local"
|
68
|
+
local_agent_knowledge_path(agent_name)
|
69
|
+
when "remote"
|
70
|
+
global_agent_knowledge_path(agent_name)
|
71
|
+
else
|
72
|
+
raise ArgumentError, "Invalid kb_type: #{kb_type}. Must be 'remote' or 'local'"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
57
76
|
private
|
58
77
|
|
59
78
|
def setup_directory_paths
|
@@ -24,21 +24,22 @@ module Aircana
|
|
24
24
|
@local_storage = Local.new
|
25
25
|
end
|
26
26
|
|
27
|
-
def fetch_pages_for(agent:)
|
27
|
+
def fetch_pages_for(agent:, kb_type: "remote")
|
28
28
|
validate_configuration!
|
29
29
|
setup_httparty
|
30
30
|
|
31
31
|
pages = search_and_log_pages(agent)
|
32
32
|
return { pages_count: 0, sources: [] } if pages.empty?
|
33
33
|
|
34
|
-
sources = process_pages_with_manifest(pages, agent)
|
35
|
-
create_or_update_manifest(agent, sources)
|
34
|
+
sources = process_pages_with_manifest(pages, agent, kb_type)
|
35
|
+
create_or_update_manifest(agent, sources, kb_type)
|
36
36
|
|
37
37
|
{ pages_count: pages.size, sources: sources }
|
38
38
|
end
|
39
39
|
|
40
40
|
def refresh_from_manifest(agent:)
|
41
41
|
sources = Manifest.sources_from_manifest(agent)
|
42
|
+
kb_type = Manifest.kb_type_from_manifest(agent)
|
42
43
|
return { pages_count: 0, sources: [] } if sources.empty?
|
43
44
|
|
44
45
|
validate_configuration!
|
@@ -55,7 +56,7 @@ module Aircana
|
|
55
56
|
|
56
57
|
return { pages_count: 0, sources: [] } if all_pages.empty?
|
57
58
|
|
58
|
-
updated_sources = process_pages_with_manifest(all_pages, agent)
|
59
|
+
updated_sources = process_pages_with_manifest(all_pages, agent, kb_type)
|
59
60
|
|
60
61
|
{ pages_count: all_pages.size, sources: updated_sources }
|
61
62
|
end
|
@@ -68,17 +69,17 @@ module Aircana
|
|
68
69
|
pages
|
69
70
|
end
|
70
71
|
|
71
|
-
def process_pages(pages, agent)
|
72
|
+
def process_pages(pages, agent, kb_type = "remote")
|
72
73
|
ProgressTracker.with_batch_progress(pages, "Processing pages") do |page, _index|
|
73
|
-
store_page_as_markdown(page, agent)
|
74
|
+
store_page_as_markdown(page, agent, kb_type)
|
74
75
|
end
|
75
76
|
end
|
76
77
|
|
77
|
-
def process_pages_with_manifest(pages, agent)
|
78
|
+
def process_pages_with_manifest(pages, agent, kb_type = "remote")
|
78
79
|
page_metadata = []
|
79
80
|
|
80
81
|
ProgressTracker.with_batch_progress(pages, "Processing pages") do |page, _index|
|
81
|
-
store_page_as_markdown(page, agent)
|
82
|
+
store_page_as_markdown(page, agent, kb_type)
|
82
83
|
page_metadata << extract_page_metadata(page)
|
83
84
|
end
|
84
85
|
|
@@ -112,11 +113,11 @@ module Aircana
|
|
112
113
|
]
|
113
114
|
end
|
114
115
|
|
115
|
-
def create_or_update_manifest(agent, sources)
|
116
|
+
def create_or_update_manifest(agent, sources, kb_type = "remote")
|
116
117
|
if Manifest.manifest_exists?(agent)
|
117
|
-
Manifest.update_manifest(agent, sources)
|
118
|
+
Manifest.update_manifest(agent, sources, kb_type: kb_type)
|
118
119
|
else
|
119
|
-
Manifest.create_manifest(agent, sources)
|
120
|
+
Manifest.create_manifest(agent, sources, kb_type: kb_type)
|
120
121
|
end
|
121
122
|
end
|
122
123
|
|
@@ -21,14 +21,15 @@ module Aircana
|
|
21
21
|
Aircana.human_logger.info "Found #{count} pages for agent '#{agent}'"
|
22
22
|
end
|
23
23
|
|
24
|
-
def store_page_as_markdown(page, agent)
|
24
|
+
def store_page_as_markdown(page, agent, kb_type = "remote")
|
25
25
|
content = page&.dig("body", "storage", "value") || fetch_page_content(page&.[]("id"))
|
26
26
|
markdown_content = convert_to_markdown(content)
|
27
27
|
|
28
28
|
@local_storage.store_content(
|
29
29
|
title: page&.[]("title"),
|
30
30
|
content: markdown_content,
|
31
|
-
agent: agent
|
31
|
+
agent: agent,
|
32
|
+
kb_type: kb_type
|
32
33
|
)
|
33
34
|
end
|
34
35
|
end
|
@@ -5,8 +5,8 @@ require "fileutils"
|
|
5
5
|
module Aircana
|
6
6
|
module Contexts
|
7
7
|
class Local
|
8
|
-
def store_content(title:, content:, agent:)
|
9
|
-
agent_dir = create_agent_knowledge_dir(agent)
|
8
|
+
def store_content(title:, content:, agent:, kb_type: "remote")
|
9
|
+
agent_dir = create_agent_knowledge_dir(agent, kb_type)
|
10
10
|
filename = sanitize_filename(title)
|
11
11
|
filepath = File.join(agent_dir, "#{filename}.md")
|
12
12
|
|
@@ -18,10 +18,10 @@ module Aircana
|
|
18
18
|
|
19
19
|
private
|
20
20
|
|
21
|
-
def create_agent_knowledge_dir(agent)
|
21
|
+
def create_agent_knowledge_dir(agent, kb_type = "remote")
|
22
22
|
config = Aircana.configuration
|
23
|
-
#
|
24
|
-
agent_dir = config.
|
23
|
+
# Route to appropriate directory based on kb_type
|
24
|
+
agent_dir = config.agent_knowledge_path(agent, kb_type)
|
25
25
|
|
26
26
|
FileUtils.mkdir_p(agent_dir)
|
27
27
|
|
@@ -7,31 +7,36 @@ module Aircana
|
|
7
7
|
module Contexts
|
8
8
|
class Manifest
|
9
9
|
class << self
|
10
|
-
def create_manifest(agent, sources)
|
10
|
+
def create_manifest(agent, sources, kb_type: "remote")
|
11
11
|
validate_sources(sources)
|
12
|
+
validate_kb_type(kb_type)
|
12
13
|
|
13
14
|
manifest_path = manifest_path_for(agent)
|
14
|
-
manifest_data = build_manifest_data(agent, sources)
|
15
|
+
manifest_data = build_manifest_data(agent, sources, kb_type)
|
15
16
|
|
16
17
|
FileUtils.mkdir_p(File.dirname(manifest_path))
|
17
18
|
File.write(manifest_path, JSON.pretty_generate(manifest_data))
|
18
19
|
|
19
|
-
Aircana.human_logger.info "Created knowledge manifest for agent '#{agent}'"
|
20
|
+
Aircana.human_logger.info "Created knowledge manifest for agent '#{agent}' (kb_type: #{kb_type})"
|
20
21
|
manifest_path
|
21
22
|
end
|
22
23
|
|
23
|
-
def update_manifest(agent, sources)
|
24
|
+
def update_manifest(agent, sources, kb_type: nil)
|
24
25
|
validate_sources(sources)
|
25
26
|
|
26
27
|
manifest_path = manifest_path_for(agent)
|
27
28
|
|
28
29
|
if File.exist?(manifest_path)
|
29
30
|
existing_data = JSON.parse(File.read(manifest_path))
|
30
|
-
|
31
|
+
# Preserve existing kb_type unless explicitly provided
|
32
|
+
kb_type_to_use = kb_type || existing_data["kb_type"] || "remote"
|
33
|
+
manifest_data = existing_data.merge({ "sources" => sources, "kb_type" => kb_type_to_use })
|
31
34
|
else
|
32
|
-
|
35
|
+
kb_type_to_use = kb_type || "remote"
|
36
|
+
manifest_data = build_manifest_data(agent, sources, kb_type_to_use)
|
33
37
|
end
|
34
38
|
|
39
|
+
validate_kb_type(manifest_data["kb_type"])
|
35
40
|
FileUtils.mkdir_p(File.dirname(manifest_path))
|
36
41
|
File.write(manifest_path, JSON.pretty_generate(manifest_data))
|
37
42
|
manifest_path
|
@@ -61,6 +66,13 @@ module Aircana
|
|
61
66
|
manifest["sources"] || []
|
62
67
|
end
|
63
68
|
|
69
|
+
def kb_type_from_manifest(agent)
|
70
|
+
manifest = read_manifest(agent)
|
71
|
+
return "remote" unless manifest
|
72
|
+
|
73
|
+
manifest["kb_type"] || "remote"
|
74
|
+
end
|
75
|
+
|
64
76
|
def manifest_exists?(agent)
|
65
77
|
File.exist?(manifest_path_for(agent))
|
66
78
|
end
|
@@ -76,10 +88,11 @@ module Aircana
|
|
76
88
|
File.join(Aircana.configuration.agent_knowledge_dir, agent)
|
77
89
|
end
|
78
90
|
|
79
|
-
def build_manifest_data(agent, sources)
|
91
|
+
def build_manifest_data(agent, sources, kb_type = "remote")
|
80
92
|
{
|
81
93
|
"version" => "1.0",
|
82
94
|
"agent" => agent,
|
95
|
+
"kb_type" => kb_type,
|
83
96
|
"sources" => sources
|
84
97
|
}
|
85
98
|
end
|
@@ -95,6 +108,10 @@ module Aircana
|
|
95
108
|
raise ManifestError, "Unsupported manifest version: #{manifest_data["version"]}"
|
96
109
|
end
|
97
110
|
|
111
|
+
# kb_type is optional for backward compatibility, defaults to "remote"
|
112
|
+
kb_type = manifest_data["kb_type"] || "remote"
|
113
|
+
validate_kb_type(kb_type)
|
114
|
+
|
98
115
|
validate_sources(manifest_data["sources"])
|
99
116
|
end
|
100
117
|
|
@@ -144,6 +161,13 @@ module Aircana
|
|
144
161
|
|
145
162
|
raise ManifestError, "URL entry missing required field: url" unless url_entry.key?("url")
|
146
163
|
end
|
164
|
+
|
165
|
+
def validate_kb_type(kb_type)
|
166
|
+
valid_types = %w[remote local]
|
167
|
+
return if valid_types.include?(kb_type)
|
168
|
+
|
169
|
+
raise ManifestError, "Invalid kb_type: #{kb_type}. Must be one of: #{valid_types.join(", ")}"
|
170
|
+
end
|
147
171
|
end
|
148
172
|
end
|
149
173
|
|