agentf 0.5.0 → 0.7.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.
@@ -12,8 +12,9 @@ module Agentf
12
12
  # - by_type accepts business_intent and feature_intent (finding #11)
13
13
  class Memory
14
14
  include ArgParser
15
+ include Agentf::Memory::ConfirmationHandler
15
16
 
16
- VALID_EPISODE_TYPES = %w[pitfall lesson success business_intent feature_intent].freeze
17
+ VALID_EPISODE_TYPES = %w[episode lesson playbook business_intent feature_intent incident].freeze
17
18
 
18
19
  def initialize(reviewer: nil, memory: nil)
19
20
  @reviewer = reviewer || Commands::MemoryReviewer.new
@@ -28,30 +29,28 @@ module Agentf
28
29
  case command
29
30
  when "recent", "list"
30
31
  list_memories(args)
31
- when "pitfalls"
32
- list_pitfalls(args)
32
+ when "episodes"
33
+ list_episodes(args)
33
34
  when "lessons"
34
35
  list_lessons(args)
35
- when "successes"
36
- list_successes(args)
37
36
  when "intents"
38
37
  list_intents(args)
39
38
  when "business-intents"
40
39
  list_business_intents(args)
41
40
  when "feature-intents"
42
41
  list_feature_intents(args)
42
+ when "add-intent"
43
+ add_intent(args)
44
+ when "add-episode"
45
+ add_episode_direct(args)
43
46
  when "add-business-intent"
44
47
  add_business_intent(args)
45
48
  when "add-feature-intent"
46
49
  add_feature_intent(args)
50
+ when "add-playbook"
51
+ add_playbook(args)
47
52
  when "add-lesson"
48
53
  add_episode("lesson", args)
49
- when "add-success"
50
- add_episode("success", args)
51
- when "add-pitfall"
52
- add_episode("pitfall", args)
53
- when "tags"
54
- list_tags
55
54
  when "search"
56
55
  search_memories(args)
57
56
  when "delete"
@@ -62,8 +61,6 @@ module Agentf
62
61
  subgraph(args)
63
62
  when "summary", "stats"
64
63
  show_summary
65
- when "by-tag"
66
- by_tag(args)
67
64
  when "by-agent"
68
65
  by_agent(args)
69
66
  when "by-type"
@@ -86,9 +83,10 @@ module Agentf
86
83
  output(result)
87
84
  end
88
85
 
89
- def list_pitfalls(args)
86
+ def list_episodes(args)
90
87
  limit = extract_limit(args)
91
- result = @reviewer.get_pitfalls(limit: limit)
88
+ outcome = parse_single_option(args, "--outcome=")
89
+ result = @reviewer.get_episodes(limit: limit, outcome: outcome)
92
90
  output(result)
93
91
  end
94
92
 
@@ -98,12 +96,6 @@ module Agentf
98
96
  output(result)
99
97
  end
100
98
 
101
- def list_successes(args)
102
- limit = extract_limit(args)
103
- result = @reviewer.get_successes(limit: limit)
104
- output(result)
105
- end
106
-
107
99
  def list_intents(args)
108
100
  limit = extract_limit(args)
109
101
  kind = args.shift
@@ -132,6 +124,24 @@ module Agentf
132
124
  output(@reviewer.get_feature_intents(limit: limit))
133
125
  end
134
126
 
127
+ def add_intent(args)
128
+ kind = args.shift.to_s.downcase
129
+ unless %w[business feature].include?(kind)
130
+ $stderr.puts "Error: add-intent requires kind (business|feature) as first argument"
131
+ exit 1
132
+ end
133
+ kind == "business" ? add_business_intent(args) : add_feature_intent(args)
134
+ end
135
+
136
+ def add_episode_direct(args)
137
+ type = args.shift.to_s
138
+ unless VALID_EPISODE_TYPES.include?(type)
139
+ $stderr.puts "Error: add-episode requires type as first argument (#{VALID_EPISODE_TYPES.join('|')})"
140
+ exit 1
141
+ end
142
+ add_episode(type, args)
143
+ end
144
+
135
145
  def add_business_intent(args)
136
146
  title = args.shift
137
147
  description = args.shift
@@ -141,16 +151,14 @@ module Agentf
141
151
  exit 1
142
152
  end
143
153
 
144
- tags = parse_list_option(args, "--tags=")
145
154
  constraints = parse_list_option(args, "--constraints=")
146
155
  priority = parse_integer_option(args, "--priority=", default: 1)
147
156
 
148
157
  id = nil
149
- res = safe_cli_memory_write(@memory, attempted: { command: "add-business-intent", args: { title: title, description: description, tags: tags, constraints: constraints, priority: priority } }) do
158
+ res = safe_memory_write(@memory, attempted: { command: "add-business-intent", args: { title: title, description: description, constraints: constraints, priority: priority } }) do
150
159
  id = @memory.store_business_intent(
151
160
  title: title,
152
161
  description: description,
153
- tags: tags,
154
162
  constraints: constraints,
155
163
  priority: priority
156
164
  )
@@ -181,17 +189,15 @@ module Agentf
181
189
  exit 1
182
190
  end
183
191
 
184
- tags = parse_list_option(args, "--tags=")
185
192
  acceptance_criteria = parse_list_option(args, "--acceptance=")
186
193
  non_goals = parse_list_option(args, "--non-goals=")
187
194
  related_task_id = parse_single_option(args, "--task=")
188
195
 
189
196
  id = nil
190
- res = safe_cli_memory_write(@memory, attempted: { command: "add-feature-intent", args: { title: title, description: description, tags: tags, acceptance: acceptance_criteria, non_goals: non_goals, related_task_id: related_task_id } }) do
197
+ res = safe_memory_write(@memory, attempted: { command: "add-feature-intent", args: { title: title, description: description, acceptance: acceptance_criteria, non_goals: non_goals, related_task_id: related_task_id } }) do
191
198
  id = @memory.store_feature_intent(
192
199
  title: title,
193
200
  description: description,
194
- tags: tags,
195
201
  acceptance_criteria: acceptance_criteria,
196
202
  non_goals: non_goals,
197
203
  related_task_id: related_task_id
@@ -214,6 +220,46 @@ module Agentf
214
220
  end
215
221
  end
216
222
 
223
+ def add_playbook(args)
224
+ title = args.shift
225
+ description = args.shift
226
+
227
+ if title.to_s.empty? || description.to_s.empty?
228
+ $stderr.puts "Error: add-playbook requires <title> <description>"
229
+ exit 1
230
+ end
231
+
232
+ steps = parse_list_option(args, "--steps=")
233
+ feature_area = parse_single_option(args, "--feature-area=")
234
+ agent = parse_single_option(args, "--agent=") || Agentf::AgentRoles::PLANNER
235
+
236
+ id = nil
237
+ res = safe_memory_write(@memory, attempted: { command: "add-playbook", args: { title: title, description: description, steps: steps, feature_area: feature_area, agent: agent } }) do
238
+ id = @memory.store_playbook(
239
+ title: title,
240
+ description: description,
241
+ steps: steps,
242
+ feature_area: feature_area,
243
+ agent: agent
244
+ )
245
+ end
246
+
247
+ if res.is_a?(Hash) && res["confirmation_required"]
248
+ if @json_output
249
+ puts JSON.generate(res)
250
+ else
251
+ $stderr.puts "Confirmation required to store playbook: #{res['confirmation_details'].inspect}"
252
+ end
253
+ return
254
+ end
255
+
256
+ if @json_output
257
+ puts JSON.generate({ "id" => id, "type" => "playbook", "status" => "stored" })
258
+ else
259
+ puts "Stored playbook: #{id}"
260
+ end
261
+ end
262
+
217
263
  def add_episode(type, args)
218
264
  title = args.shift
219
265
  description = args.shift
@@ -223,21 +269,21 @@ module Agentf
223
269
  exit 1
224
270
  end
225
271
 
226
- tags = parse_list_option(args, "--tags=")
227
272
  context = parse_single_option(args, "--context=").to_s
228
273
  agent = parse_single_option(args, "--agent=") || Agentf::AgentRoles::ENGINEER
229
274
  code_snippet = parse_single_option(args, "--code=").to_s
275
+ outcome = parse_single_option(args, "--outcome=")
230
276
 
231
277
  id = nil
232
- res = safe_cli_memory_write(@memory, attempted: { command: "add-#{type}", args: { title: title, description: description, tags: tags, context: context, agent: agent, code: code_snippet } }) do
278
+ res = safe_memory_write(@memory, attempted: { command: "add-#{type}", args: { title: title, description: description, context: context, agent: agent, code: code_snippet, outcome: outcome } }) do
233
279
  id = @memory.store_episode(
234
280
  type: type,
235
281
  title: title,
236
282
  description: description,
237
283
  context: context,
238
- tags: tags,
239
284
  agent: agent,
240
- code_snippet: code_snippet
285
+ code_snippet: code_snippet,
286
+ outcome: outcome
241
287
  )
242
288
  end
243
289
 
@@ -257,37 +303,6 @@ module Agentf
257
303
  end
258
304
  end
259
305
 
260
- # Helper to standardize CLI memory write confirmation handling.
261
- def safe_cli_memory_write(memory, attempted: {})
262
- begin
263
- yield
264
- nil
265
- rescue Agentf::Memory::RedisMemory::ConfirmationRequired => e
266
- {
267
- "confirmation_required" => true,
268
- "confirmation_details" => e.details,
269
- "attempted" => attempted,
270
- "confirmed_write_token" => "confirmed",
271
- "confirmation_prompt" => "Ask the user whether to save this memory. If they approve, rerun the same command with confirmation enabled. If they decline, do not retry."
272
- }
273
- end
274
- end
275
-
276
- def list_tags
277
- result = @reviewer.get_all_tags
278
- if @json_output
279
- puts JSON.generate(result)
280
- return
281
- end
282
-
283
- if result["tags"].empty?
284
- puts "No tags found."
285
- else
286
- puts "Tags (#{result["count"]}):"
287
- result["tags"].each { |tag| puts " - #{tag}" }
288
- end
289
- end
290
-
291
306
  def search_memories(args)
292
307
  # Extract limit BEFORE joining remaining args as query (fixes finding #7)
293
308
  limit = extract_limit(args)
@@ -318,19 +333,12 @@ module Agentf
318
333
  puts ""
319
334
  puts "By agent:"
320
335
  result["by_agent"].each { |agent, count| puts " #{agent}: #{count}" }
321
- puts ""
322
- puts "Unique tags: #{result["unique_tags"]}"
323
- end
324
336
 
325
- def by_tag(args)
326
- tag = args.shift
327
- if tag.nil? || tag.empty?
328
- $stderr.puts "Error: by-tag requires a tag name"
329
- exit 1
337
+ if result["by_outcome"].is_a?(Hash)
338
+ puts ""
339
+ puts "By outcome:"
340
+ result["by_outcome"].each { |outcome, count| puts " #{outcome}: #{count}" }
330
341
  end
331
- limit = extract_limit(args)
332
- result = @reviewer.get_by_tag(tag, limit: limit)
333
- output(result)
334
342
  end
335
343
 
336
344
  def by_agent(args)
@@ -540,8 +548,8 @@ module Agentf
540
548
  [#{mem["type"]&.upcase}] #{mem["title"]}
541
549
  #{mem["created_at"]} by #{mem["agent"]}
542
550
  #{mem["description"]}
551
+ #{"Outcome: #{mem['outcome']}" unless mem["outcome"].to_s.empty?}
543
552
  #{format_code(mem["code_snippet"]) unless mem["code_snippet"].to_s.empty?}
544
- Tags: #{mem["tags"]&.join(", ") || "none"}
545
553
  OUTPUT
546
554
  end
547
555
 
@@ -557,26 +565,20 @@ module Agentf
557
565
 
558
566
  Commands:
559
567
  recent, list List recent memories (default: 10)
560
- pitfalls List pitfalls (things that went wrong)
568
+ episodes List episode memories
561
569
  lessons List lessons learned
562
- successes List successes
563
570
  intents [kind] List intents (kind: business|feature)
564
- business-intents List business intents
565
- feature-intents List feature intents
566
- add-business-intent Store business intent
567
- add-feature-intent Store feature intent
571
+ add-intent <kind> Store intent (kind: business|feature)
572
+ add-episode <type> Store episode (type: #{VALID_EPISODE_TYPES.join("|")}) with --outcome=
573
+ add-playbook Store playbook memory
568
574
  add-lesson Store lesson memory
569
- add-success Store success memory
570
- add-pitfall Store pitfall memory
571
- tags List all unique tags
572
- search <query> Search memories by keyword
575
+ search <query> Search memories semantically
573
576
  delete id <memory_id> Delete one memory and related edges
574
577
  delete last -n <count> Delete most recent memories
575
578
  delete all Delete memories and graph/task keys
576
579
  neighbors <id> Traverse graph edges from a memory id
577
580
  subgraph <ids> Build graph from comma-separated seed ids
578
581
  summary, stats Show summary statistics
579
- by-tag <tag> Get memories with specific tag
580
582
  by-agent <agent> Get memories from specific agent
581
583
  by-type <type> Get memories by type (#{VALID_EPISODE_TYPES.join("|")})
582
584
 
@@ -586,18 +588,19 @@ module Agentf
586
588
 
587
589
  Examples:
588
590
  agentf memory recent -n 5
589
- agentf memory pitfalls
591
+ agentf memory episodes --outcome=negative
590
592
  agentf memory intents business -n 5
591
- agentf memory add-business-intent "Reliability" "Prioritize uptime" --tags=ops,platform --constraints="No downtime;No vendor lock-in"
592
- agentf memory add-feature-intent "Agent handoff" "Improve orchestrator continuity" --acceptance="Keeps context;Preserves task state"
593
- agentf memory add-lesson "Refactor strategy" "Extracted adapter seam" --agent=PLANNER --tags=architecture
594
- agentf memory add-success "Provider install works" "Installed copilot + opencode manifests" --agent=ENGINEER
595
- agentf memory search "react"
593
+ agentf memory add-intent business "Reliability" "Prioritize uptime" --constraints="No downtime;No vendor lock-in"
594
+ agentf memory add-intent feature "Agent handoff" "Improve continuity" --acceptance="Keeps context;Preserves task state"
595
+ agentf memory add-episode lesson "Refactor strategy" "Extracted adapter seam" --agent=PLANNER --outcome=positive
596
+ agentf memory add-episode incident "Auth regression" "JWT expiry not checked" --outcome=negative
597
+ agentf memory add-playbook "Release rollout" "Safe deploy sequence" --steps="deploy canary;monitor;promote"
598
+ agentf memory add-lesson "Refactor strategy" "Extracted adapter seam" --agent=PLANNER
599
+ agentf memory search "react" --type=lesson --outcome=positive
596
600
  agentf memory delete id episode_abcd
597
601
  agentf memory delete last -n 10 --scope=project
598
602
  agentf memory delete all --scope=all --yes
599
603
  agentf memory neighbors episode_abcd --depth=2
600
- agentf memory by-tag "performance"
601
604
  agentf memory summary
602
605
  HELP
603
606
  end
@@ -75,7 +75,7 @@ module Agentf
75
75
  Usage: agentf <command> [subcommand] [options]
76
76
 
77
77
  Commands:
78
- memory Manage agent memory (lessons, pitfalls, successes, intents)
78
+ memory Manage agent memory (episodes, lessons, playbooks, intents)
79
79
  code Explore codebase (glob, grep, tree, related files)
80
80
  metrics Show workflow success and provider parity metrics
81
81
  architecture Analyze architecture layers and violations
@@ -12,16 +12,14 @@ module Agentf
12
12
  def self.manifest
13
13
  {
14
14
  "name" => NAME,
15
- "description" => "Review and query Redis-stored memories, pitfalls, and learnings.",
15
+ "description" => "Review and query Redis-stored memories, episodes, and learnings.",
16
16
  "commands" => [
17
17
  { "name" => "get_recent_memories", "type" => "function" },
18
- { "name" => "get_pitfalls", "type" => "function" },
18
+ { "name" => "get_episodes", "type" => "function" },
19
19
  { "name" => "get_lessons", "type" => "function" },
20
- { "name" => "get_successes", "type" => "function" },
21
- { "name" => "get_all_tags", "type" => "function" },
22
- { "name" => "get_by_tag", "type" => "function" },
23
20
  { "name" => "get_by_type", "type" => "function" },
24
21
  { "name" => "get_by_agent", "type" => "function" },
22
+ { "name" => "get_intents", "type" => "function" },
25
23
  { "name" => "search", "type" => "function" },
26
24
  { "name" => "get_summary", "type" => "function" },
27
25
  { "name" => "neighbors", "type" => "function" },
@@ -44,10 +42,9 @@ module Agentf
44
42
  { "error" => e.message }
45
43
  end
46
44
 
47
- # Get all pitfalls (things that went wrong)
48
- def get_pitfalls(limit: 10)
49
- pitfalls = @memory.get_pitfalls(limit: limit)
50
- format_memories(pitfalls)
45
+ def get_episodes(limit: 10, outcome: nil)
46
+ episodes = @memory.get_episodes(limit: limit, outcome: outcome)
47
+ format_memories(episodes)
51
48
  rescue => e
52
49
  { "error" => e.message }
53
50
  end
@@ -60,14 +57,6 @@ module Agentf
60
57
  { "error" => e.message }
61
58
  end
62
59
 
63
- # Get all successes
64
- def get_successes(limit: 10)
65
- successes = @memory.get_memories_by_type(type: "success", limit: limit)
66
- format_memories(successes)
67
- rescue => e
68
- { "error" => e.message }
69
- end
70
-
71
60
  def get_business_intents(limit: 10)
72
61
  intents = @memory.get_intents(kind: "business", limit: limit)
73
62
  format_memories(intents)
@@ -89,51 +78,25 @@ module Agentf
89
78
  { "error" => e.message }
90
79
  end
91
80
 
92
- # Get all unique tags from memories
93
- def get_all_tags
94
- tags = @memory.get_all_tags
95
- { "tags" => tags.sort, "count" => tags.length }
96
- rescue => e
97
- { "error" => e.message }
98
- end
99
-
100
- # Get memories by tag
101
- def get_by_tag(tag, limit: 10)
102
- memories = @memory.get_recent_memories(limit: 100)
103
- filtered = memories.select { |m| m["tags"]&.include?(tag) }
104
- format_memories(filtered.first(limit))
105
- rescue => e
106
- { "error" => e.message }
107
- end
108
-
109
81
  # Get memories by type (pitfall, lesson, success)
110
82
  def get_by_type(type, limit: 10)
111
- memories = @memory.get_recent_memories(limit: 100)
112
- filtered = memories.select { |m| m["type"] == type }
113
- format_memories(filtered.first(limit))
83
+ memories = @memory.get_memories_by_type(type: type, limit: limit)
84
+ format_memories(memories)
114
85
  rescue => e
115
86
  { "error" => e.message }
116
87
  end
117
88
 
118
89
  # Get memories by agent
119
90
  def get_by_agent(agent, limit: 10)
120
- memories = @memory.get_recent_memories(limit: 100)
121
- filtered = memories.select { |m| m["agent"] == agent }
122
- format_memories(filtered.first(limit))
91
+ memories = @memory.get_memories_by_agent(agent: agent, limit: limit)
92
+ format_memories(memories)
123
93
  rescue => e
124
94
  { "error" => e.message }
125
95
  end
126
96
 
127
- # Search memories by keyword in title or description
128
- def search(query, limit: 10)
129
- memories = @memory.get_recent_memories(limit: 100)
130
- q = query.downcase
131
- filtered = memories.select do |m|
132
- m["title"]&.downcase&.include?(q) ||
133
- m["description"]&.downcase&.include?(q) ||
134
- m["context"]&.downcase&.include?(q)
135
- end
136
- format_memories(filtered.first(limit))
97
+ # Search memories semantically with optional type, agent, and outcome filters
98
+ def search(query, limit: 10, type: nil, agent: nil, outcome: nil)
99
+ format_memories(@memory.search_memories(query: query, limit: limit, type: type, agent: agent, outcome: outcome))
137
100
  rescue => e
138
101
  { "error" => e.message }
139
102
  end
@@ -141,19 +104,22 @@ module Agentf
141
104
  # Get summary statistics
142
105
  def get_summary
143
106
  memories = @memory.get_recent_memories(limit: 100)
144
- tags = @memory.get_all_tags
145
107
 
146
108
  {
147
109
  "total_memories" => memories.length,
148
110
  "by_type" => {
149
- "pitfall" => memories.count { |m| m["type"] == "pitfall" },
111
+ "episode" => memories.count { |m| m["type"] == "episode" },
150
112
  "lesson" => memories.count { |m| m["type"] == "lesson" },
151
- "success" => memories.count { |m| m["type"] == "success" },
113
+ "playbook" => memories.count { |m| m["type"] == "playbook" },
152
114
  "business_intent" => memories.count { |m| m["type"] == "business_intent" },
153
115
  "feature_intent" => memories.count { |m| m["type"] == "feature_intent" }
154
116
  },
117
+ "by_outcome" => {
118
+ "positive" => memories.count { |m| m["outcome"] == "positive" },
119
+ "negative" => memories.count { |m| m["outcome"] == "negative" },
120
+ "neutral" => memories.count { |m| m["outcome"] == "neutral" }
121
+ },
155
122
  "by_agent" => memories.each_with_object(Hash.new(0)) { |m, h| h[m["agent"]] += 1 },
156
- "unique_tags" => tags.length,
157
123
  "project" => @project
158
124
  }
159
125
  rescue => e
@@ -189,7 +155,7 @@ module Agentf
189
155
  "description" => m["description"],
190
156
  "context" => m["context"],
191
157
  "code_snippet" => m["code_snippet"],
192
- "tags" => m["tags"],
158
+ "outcome" => m["outcome"],
193
159
  "agent" => m["agent"],
194
160
  "metadata" => m["metadata"],
195
161
  "entity_ids" => m["entity_ids"],
@@ -7,8 +7,6 @@ module Agentf
7
7
  class Metrics
8
8
  NAME = "metrics"
9
9
 
10
- WORKFLOW_METRICS_TAG = "workflow_metric"
11
-
12
10
  def self.manifest
13
11
  {
14
12
  "name" => NAME,
@@ -30,12 +28,13 @@ module Agentf
30
28
  metrics = extract_metrics(workflow_state)
31
29
  begin
32
30
  @memory.store_episode(
33
- type: "success",
31
+ type: "episode",
34
32
  title: metric_title(metrics),
35
33
  description: metric_description(metrics),
36
34
  context: metric_context(metrics),
37
- tags: metric_tags(metrics),
38
35
  agent: Agentf::AgentRoles::ORCHESTRATOR,
36
+ outcome: "positive",
37
+ metadata: { "workflow_metric" => true },
39
38
  code_snippet: ""
40
39
  )
41
40
  { "status" => "recorded", "metrics" => metrics }
@@ -171,14 +170,6 @@ module Agentf
171
170
  }.to_json
172
171
  end
173
172
 
174
- def metric_tags(metrics)
175
- [
176
- WORKFLOW_METRICS_TAG,
177
- "provider:#{metrics['provider'].to_s.downcase}",
178
- "workflow:#{metrics['workflow_type']}"
179
- ]
180
- end
181
-
182
173
  def top_contract_violations(records)
183
174
  counts = Hash.new(0)
184
175
  records.each do |record|
@@ -191,7 +182,7 @@ module Agentf
191
182
  memories = @memory.get_recent_memories(limit: limit)
192
183
 
193
184
  memories
194
- .select { |m| Array(m["tags"]).include?(WORKFLOW_METRICS_TAG) }
185
+ .select { |m| m.dig("metadata", "workflow_metric") == true }
195
186
  .map do |m|
196
187
  context = parse_context_json(m["context"])
197
188
  context
@@ -2,8 +2,9 @@
2
2
 
3
3
  module Agentf
4
4
  class ContextBuilder
5
- def initialize(memory:)
5
+ def initialize(memory:, embedding_provider: Agentf::EmbeddingProvider.new)
6
6
  @memory = memory
7
+ @embedding_provider = embedding_provider
7
8
  end
8
9
 
9
10
  def build(agent:, workflow_state:, limit: 8)
@@ -13,23 +14,12 @@ module Agentf
13
14
  @memory.get_agent_context(
14
15
  agent: agent,
15
16
  task_type: task_type,
16
- query_embedding: simple_embedding(task),
17
+ query_text: task,
18
+ query_embedding: @embedding_provider.embed(task),
17
19
  limit: limit
18
20
  )
19
21
  rescue StandardError
20
22
  { "agent" => agent, "intent" => [], "memories" => [], "similar_tasks" => [] }
21
23
  end
22
-
23
- private
24
-
25
- def simple_embedding(text)
26
- normalized = text.to_s.downcase
27
- [
28
- normalized.include?("fix") || normalized.include?("bug") ? 1.0 : 0.0,
29
- normalized.include?("feature") || normalized.include?("add") ? 1.0 : 0.0,
30
- normalized.include?("security") ? 1.0 : 0.0,
31
- normalized.length.to_f / 100.0
32
- ]
33
- end
34
24
  end
35
25
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ module Agentf
6
+ class EmbeddingProvider
7
+ DIMENSIONS = 64
8
+
9
+ def initialize(dimensions: DIMENSIONS)
10
+ @dimensions = dimensions
11
+ end
12
+
13
+ def embed(text)
14
+ tokens = tokenize(text)
15
+ return [] if tokens.empty?
16
+
17
+ vector = Array.new(@dimensions, 0.0)
18
+ tokens.each do |token|
19
+ hash = Digest::SHA256.hexdigest(token)[0, 8].to_i(16)
20
+ vector[hash % @dimensions] += 1.0
21
+ end
22
+
23
+ magnitude = Math.sqrt(vector.sum { |value| value * value })
24
+ return vector if magnitude.zero?
25
+
26
+ vector.map { |value| (value / magnitude).round(8) }
27
+ end
28
+
29
+ private
30
+
31
+ def tokenize(text)
32
+ text.to_s.downcase.scan(/[a-z0-9_]+/).reject { |token| token.length < 2 }
33
+ end
34
+ end
35
+ end