ask-tokens 0.2.1 → 0.3.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/CHANGELOG.md +12 -0
- data/lib/ask/tokens/activity.rb +12 -3
- data/lib/ask/tokens/activity_registry.rb +43 -12
- data/lib/ask/tokens/version.rb +1 -1
- data/lib/ask/tokens/wallet.rb +13 -6
- data/lib/ask/tokens.rb +19 -8
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ceb47fceedf25098c5bc27e2c544feaad5226839b337a87d1cf1503fcc5331d9
|
|
4
|
+
data.tar.gz: 928470bc83407814f4a8aa8f78b24170a23789cde42ea21b63b9c8b22c160118
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e073422cdd0e0ef386a2532382644abcb77d4161b8d3e49b03983c9d13185a7dfac3743ad485acf12aa5cd3748f298f6d73d5a8c7425456e288c9596a6dc657c
|
|
7
|
+
data.tar.gz: 4acccaf24b0afe5597978ef93ad8f75117d77980be674b441b6764df95f5ab182ee1d225035e4e861d8d58af4e8ca6e631c1404ab3b51c22ead52cd58682a776
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.0] - 2026-10-20
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Activity namespacing: `Activity#namespace` and `#qualified_name` so multiple projects sharing one registry never collide on common names like `:compute_hour`. `Ask::Tokens.activity(name, namespace:, ...)` and `Ask::Tokens.estimate(name, namespace:)` resolve scoped activities.
|
|
7
|
+
- `ActivityRegistry#fetch(name, namespace:)`, `#names(namespace:)`, `#each(namespace:)`, `#clear(namespace:)`, and `#registered?(name, namespace:)` for scoped access.
|
|
8
|
+
- `Ask::Tokens#wallet_for(owner, store:)` override, so a billing service can route different owners to different backends (useful for tests and multi-tenant routing).
|
|
9
|
+
- `Wallet#estimate`, `#enough_for?`, and `#spend!` accept `namespace:` plus keyword params that are merged into the params hash.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- `Ask::Tokens#estimate` and `Wallet#estimate/#spend!` accept keyword arguments in addition to a positional params Hash.
|
|
13
|
+
|
|
14
|
+
|
|
3
15
|
## [0.1.0] - 2026-08-19
|
|
4
16
|
|
|
5
17
|
### Added
|
data/lib/ask/tokens/activity.rb
CHANGED
|
@@ -10,16 +10,25 @@ module Ask
|
|
|
10
10
|
# end
|
|
11
11
|
#
|
|
12
12
|
class Activity
|
|
13
|
-
attr_reader :name
|
|
13
|
+
attr_reader :name, :namespace
|
|
14
14
|
|
|
15
|
-
def initialize(name, cost: nil, &block)
|
|
15
|
+
def initialize(name, cost: nil, namespace: nil, &block)
|
|
16
16
|
raise ArgumentError, "activity #{name.inspect} needs a cost or a block" if cost.nil? && block.nil?
|
|
17
17
|
|
|
18
18
|
@name = name.to_sym
|
|
19
|
+
@namespace = namespace&.to_sym
|
|
19
20
|
@cost = cost
|
|
20
21
|
@block = block
|
|
21
22
|
end
|
|
22
23
|
|
|
24
|
+
# The fully-qualified name, including namespace when present.
|
|
25
|
+
# Used as the registry key so scoped and unscoped activities never
|
|
26
|
+
# collide. A billing service serving multiple projects can namespace
|
|
27
|
+
# each project's activities (e.g. :ship) without collisions.
|
|
28
|
+
def qualified_name
|
|
29
|
+
@namespace ? :"#{@namespace}:#{@name}" : @name
|
|
30
|
+
end
|
|
31
|
+
|
|
23
32
|
# The token cost of running this activity with +params+. Always returns
|
|
24
33
|
# an Integer, rounded per configuration. A fixed cost ignores params; a
|
|
25
34
|
# block receives them (zero-arity blocks are called with no arguments).
|
|
@@ -36,7 +45,7 @@ module Ask
|
|
|
36
45
|
end
|
|
37
46
|
|
|
38
47
|
def to_s
|
|
39
|
-
"Activity(#{@name})"
|
|
48
|
+
@namespace ? "Activity(#{@namespace}:#{@name})" : "Activity(#{@name})"
|
|
40
49
|
end
|
|
41
50
|
|
|
42
51
|
def inspect
|
|
@@ -3,32 +3,63 @@
|
|
|
3
3
|
module Ask
|
|
4
4
|
module Tokens
|
|
5
5
|
# Stores declared activities by name.
|
|
6
|
+
#
|
|
7
|
+
# Activities can be scoped to a namespace (e.g. a project or service
|
|
8
|
+
# name) so that multiple projects sharing one registry never collide
|
|
9
|
+
# on a common name like :compute_hour. The registry key is the
|
|
10
|
+
# activity's qualified name (namespace:name when namespaced).
|
|
6
11
|
class ActivityRegistry
|
|
7
12
|
def initialize
|
|
8
13
|
@activities = {}
|
|
9
14
|
end
|
|
10
15
|
|
|
11
|
-
def register(name, cost: nil, &block)
|
|
12
|
-
activity = Activity.new(name, cost: cost, &block)
|
|
13
|
-
@activities[activity.
|
|
16
|
+
def register(name, cost: nil, namespace: nil, &block)
|
|
17
|
+
activity = Activity.new(name, cost: cost, namespace: namespace, &block)
|
|
18
|
+
@activities[activity.qualified_name] = activity
|
|
14
19
|
end
|
|
15
20
|
|
|
16
|
-
def fetch(name)
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
def fetch(name, namespace: nil)
|
|
22
|
+
key = namespace ? :"#{namespace}:#{name}" : name.to_sym
|
|
23
|
+
@activities.fetch(key) do
|
|
24
|
+
raise KeyError, "Unknown activity: #{name.inspect} (namespace: #{namespace.inspect}). Registered: #{@activities.keys.inspect}"
|
|
19
25
|
end
|
|
20
26
|
end
|
|
21
27
|
|
|
22
|
-
|
|
23
|
-
|
|
28
|
+
# All registered activity names. Pass +namespace:+ to filter to one
|
|
29
|
+
# namespace; omit for every activity.
|
|
30
|
+
def names(namespace: nil)
|
|
31
|
+
if namespace
|
|
32
|
+
ns = namespace.to_sym
|
|
33
|
+
@activities.keys.select { |k| k.to_s.start_with?("#{ns}:") }
|
|
34
|
+
else
|
|
35
|
+
@activities.keys
|
|
36
|
+
end
|
|
24
37
|
end
|
|
25
38
|
|
|
26
|
-
|
|
27
|
-
|
|
39
|
+
# Iterate activities. Pass +namespace:+ to scope.
|
|
40
|
+
def each(namespace: nil, &block)
|
|
41
|
+
if namespace
|
|
42
|
+
ns = namespace.to_sym
|
|
43
|
+
@activities.each_value { |a| block.call(a) if a.namespace == ns }
|
|
44
|
+
else
|
|
45
|
+
@activities.each_value(&block)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Remove all activities, or only those in a namespace.
|
|
50
|
+
def clear(namespace: nil)
|
|
51
|
+
if namespace
|
|
52
|
+
ns = namespace.to_sym
|
|
53
|
+
@activities.delete_if { |_, a| a.namespace == ns }
|
|
54
|
+
else
|
|
55
|
+
@activities.clear
|
|
56
|
+
end
|
|
28
57
|
end
|
|
29
58
|
|
|
30
|
-
|
|
31
|
-
|
|
59
|
+
# True when an activity (optionally in a namespace) is registered.
|
|
60
|
+
def registered?(name, namespace: nil)
|
|
61
|
+
key = namespace ? :"#{namespace}:#{name}" : name.to_sym
|
|
62
|
+
@activities.key?(key)
|
|
32
63
|
end
|
|
33
64
|
end
|
|
34
65
|
end
|
data/lib/ask/tokens/version.rb
CHANGED
data/lib/ask/tokens/wallet.rb
CHANGED
|
@@ -38,13 +38,17 @@ module Ask
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
# Estimated token cost of running +activity+ with +params+.
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
# Pass +namespace:+ to resolve a namespaced activity.
|
|
42
|
+
# Params may be passed positionally as a Hash or as keyword arguments.
|
|
43
|
+
def estimate(activity_name, params = {}, namespace: nil, **extra)
|
|
44
|
+
params = params.merge(extra) unless extra.empty?
|
|
45
|
+
Ask::Tokens.estimate(activity_name, params, namespace: namespace)
|
|
43
46
|
end
|
|
44
47
|
|
|
45
48
|
# True when the balance covers the estimated cost of an activity.
|
|
46
|
-
def enough_for?(activity_name, params = {})
|
|
47
|
-
|
|
49
|
+
def enough_for?(activity_name, params = {}, namespace: nil, **extra)
|
|
50
|
+
params = params.merge(extra) unless extra.empty?
|
|
51
|
+
has?(estimate(activity_name, params, namespace: namespace))
|
|
48
52
|
end
|
|
49
53
|
|
|
50
54
|
# Total tokens debited since +time+ (absolute value). Useful for
|
|
@@ -84,8 +88,11 @@ module Ask
|
|
|
84
88
|
# Spend the estimated token cost of +activity+ given +params+. With a
|
|
85
89
|
# block, charge only if the block succeeds — the standard wrapper for
|
|
86
90
|
# LLM calls and render jobs.
|
|
87
|
-
|
|
88
|
-
|
|
91
|
+
#
|
|
92
|
+
# Pass +namespace:+ to resolve a namespaced activity.
|
|
93
|
+
def spend!(activity_name, params = {}, namespace: nil, **extra, &block)
|
|
94
|
+
params = params.merge(extra) unless extra.empty?
|
|
95
|
+
amount = estimate(activity_name, params, namespace: namespace)
|
|
89
96
|
deduct!(amount, reason: activity_name.to_s, metadata: params.dup, &block)
|
|
90
97
|
end
|
|
91
98
|
|
data/lib/ask/tokens.rb
CHANGED
|
@@ -85,14 +85,23 @@ module Ask
|
|
|
85
85
|
|
|
86
86
|
# Register an activity. +cost:+ is a fixed integer; without it the block
|
|
87
87
|
# receives params and must return an integer token count.
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
#
|
|
89
|
+
# Pass +namespace:+ to scope the activity to a project or service so
|
|
90
|
+
# that multiple projects sharing one registry never collide on a
|
|
91
|
+
# common name like :compute_hour:
|
|
92
|
+
#
|
|
93
|
+
# Ask::Tokens.activity(:compute_hour, namespace: :ship, cost: 34)
|
|
94
|
+
#
|
|
95
|
+
def activity(name, cost: nil, namespace: nil, &block)
|
|
96
|
+
activities.register(name, cost: cost, namespace: namespace, &block)
|
|
90
97
|
end
|
|
91
98
|
|
|
92
99
|
# The estimated token cost of running +activity+ with +params+, without
|
|
93
|
-
# spending anything.
|
|
94
|
-
|
|
95
|
-
|
|
100
|
+
# spending anything. Pass +namespace:+ to resolve a scoped activity.
|
|
101
|
+
# Params may be passed positionally as a Hash or as keyword arguments.
|
|
102
|
+
def estimate(activity_name, params = {}, namespace: nil, **extra)
|
|
103
|
+
params = params.merge(extra) unless extra.empty?
|
|
104
|
+
activities.fetch(activity_name, namespace: namespace).cost(params)
|
|
96
105
|
end
|
|
97
106
|
|
|
98
107
|
def activities
|
|
@@ -100,9 +109,11 @@ module Ask
|
|
|
100
109
|
end
|
|
101
110
|
|
|
102
111
|
# Build a wallet for +owner+. +owner+ is an opaque identifier the store
|
|
103
|
-
# keys on.
|
|
104
|
-
|
|
105
|
-
|
|
112
|
+
# keys on. Pass +store:+ to override the configured store — useful for
|
|
113
|
+
# tests and for a billing service that routes different owners to
|
|
114
|
+
# different backends.
|
|
115
|
+
def wallet_for(owner, store: nil)
|
|
116
|
+
Wallet.new(owner: owner, store: (store || config.store), time: config.time)
|
|
106
117
|
end
|
|
107
118
|
|
|
108
119
|
# Reset configuration and activity registry (mainly for tests).
|