pinecall 0.0.4 → 0.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a2a10f0e074b767542af49bb9d9d6ae3251e230b371debee5d5e5628c92f67b2
4
- data.tar.gz: 81be17cb2c53a7c5c6fc3d188f7b38abd16f265afafc8c3d0a089933fb126809
3
+ metadata.gz: e3afca7edf6bc33bb5b6fc9ebed23d76eecac2bb6039fb243d252d379e509e50
4
+ data.tar.gz: f3a39bb373fa2565bc6f49f04429d8f560dcc5cd8d01f9cc8d874f3ae452808d
5
5
  SHA512:
6
- metadata.gz: f7baea4bcf9f7b72c71d24e24ddf698e3afa5d06863d60dbd32943b8648705857e8b209b43130fc1c9e435673f03666f50a9ee7694f970c19f6ca2eb868e4ef9
7
- data.tar.gz: e966301764b4ce05390cae2821339012f95106dea20593fc0e1ffc00106c48d348ef6e1f56ab46ca5ad6ce0f67fb1bb5072f071334f2d0ac59fdc8309269bdb3
6
+ metadata.gz: bd6ea499f8cdbe11a9faf6e25715848c470755e8318705beee191db517446096866e79bd830c32ba77ed4c38c7da134ccdacf90ae973c7169d18baa5a58abd93
7
+ data.tar.gz: 9ee25075169e9581cafd1c58a67eee8b47705cc82bbd3d6aa8568b1a35db6ac20039089bb72de8004bbd973772679725208c3fdca5d469a8b292b2de53badd63
data/CHANGELOG.md CHANGED
@@ -6,6 +6,27 @@ version number is the human's call.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.0.6] — A greeting is words or :improvise, a hangup is words or true, and the ears choose who ends the turn (2026-10-10)
10
+
11
+ - `greeting "…"` (said as written), `greeting :improvise` (the model opens on its prompt) and
12
+ `greeting improvise("…")` (with an instruction); `interruptible: true` for an opening the caller
13
+ may cut short, which by default they cannot. The `say:` / `reply:` keywords are gone.
14
+ - `hangup "…"` (when, in your words) or `hangup true` (whenever the model judges); `when:` is gone.
15
+ - `stt "…", end_of_turn: :stt | :livekit | :smart_turn`: who says the caller's turn is over, as
16
+ @pinecall/agents 0.9.26 says it. Needs runtime 0.1.43.
17
+
18
+ ## [0.0.5] — The class declares its voice, its models and the rest, and wins (2026-10-10)
19
+
20
+ - **A class declares its environment again, and it wins over the settings.** Class macros `voice
21
+ "<vendor>", "<id>", model:`, `llm "<vendor>/<model>", temperature:`, `stt "<vendor>/<model>"`,
22
+ `language`, `greeting`, `hangup`, `turn`, `says`, `hears`, `knowledge`, `docs`, `memory` and
23
+ `record`, sent on `agent.configure`. A field the class declares is not read from the settings,
24
+ the console shows it "set by the class", and `pinecall agent set` of it is refused. Needs runtime
25
+ 0.1.42.
26
+ - `builds:` and `options:` on `voice`, `llm` and `stt`: a class of the vendor's plugin and its
27
+ keyword arguments (`llm "openai/gpt-5.4-mini", builds: "responses.LLM", options: { use_websocket:
28
+ true }`), only on the org's own key for the vendor. The refusal that named a CLI verb is gone.
29
+
9
30
  ## [0.0.4] — A tool announces itself (2026-10-09)
10
31
 
11
32
  ### Added
@@ -4,35 +4,54 @@ module Pinecall
4
4
  class Agent
5
5
  # Class-level configuration (state lives on the instance).
6
6
  #
7
- # A class declares its contract: tools, state, view. Runtime settings (voice, models,
8
- # language, greeting, memory, knowledge) belong to the world and are refused at load with the
9
- # CLI command that sets them.
7
+ # A class declares its contract: tools, state, view. It may also declare its environment —
8
+ # voice, models, language, greeting, memory, knowledge — and whatever it declares wins over the
9
+ # agent's settings for that field.
10
10
  module Config
11
11
  # `phone`, `whatsapp` and `web` are accepted for compatibility but ignored. `channel_rules
12
12
  # false` leaves the `<channel>` block out of the prompt.
13
13
  AS_WRITTEN = %i[phone whatsapp web channel_rules].freeze
14
14
 
15
- # Settings that moved to the world, and the command that sets each. Must match the
16
- # TypeScript package's `THE_WORLDS`.
17
- THE_WORLDS = {
18
- voice: "pinecall agent set --voice <name>",
19
- llm: "pinecall agent set --llm <vendor/model>",
20
- stt: "pinecall agent set --stt <vendor>",
21
- language: "pinecall agent set --language <tag>",
22
- greeting: "pinecall agent set --greeting '…' (or --reply '…')",
23
- hangup: "pinecall agent set --hangup '…'",
24
- says: "pinecall lexicon add <word> --say '…'",
25
- hears: "pinecall lexicon hear <word> …",
26
- memory: "pinecall memory policy --remember '…' --forget '…'",
27
- record: "pinecall agent set --record on|off",
28
- knowledge: "pinecall agent knowledge edit — what the agent knows by heart is a setting, not a file",
29
- docs: "pinecall docs push, then pinecall docs attach <base>"
30
- }.freeze
15
+ # The settings a class may declare. Must match the TypeScript package's `ENVIRONMENT`.
16
+ ENVIRONMENT = %i[language voice llm stt greeting hangup turn says hears knowledge docs memory record].freeze
17
+
18
+ # The ones written as the wire's own fields, keyword by keyword: `greeting say: "…"`.
19
+ AS_KEYWORDS = %i[turn knowledge docs memory].freeze
20
+
21
+ NO_VOICE = "a voice the class declares names its vendor and the voice: voice \"<vendor>\", \"<voice id>\""
22
+
23
+ NO_OPENING = "a greeting is the words, as a string, or :improvise for the model's own: improvise(\"…\") gives it an instruction"
24
+ NO_ENDING = "hangup is when the model may end the call, in your words, or true whenever it judges the call done"
25
+
26
+ # The model opens the call: on its prompt alone (an empty instruction), or with this one.
27
+ Improvised = Data.define(:instruction, :interruptible)
31
28
 
32
29
  NOTHING = Object.new.freeze
33
30
 
34
- def self.moved_to_the_world(field)
35
- "`#{field}` is the world's now, not the class's: #{THE_WORLDS.fetch(field)} — remove it from the class"
31
+ # An opening as the wire says it: words to `say`, or a `reply` the model opens on.
32
+ def self.greeting_of(opening, interruptible)
33
+ said = case opening
34
+ when :improvise then { reply: "" }
35
+ when Improvised then { reply: opening.instruction }
36
+ when String then { say: opening }
37
+ else raise DeclarationRefused, NO_OPENING
38
+ end
39
+ given = opening.is_a?(Improvised) ? opening.interruptible : interruptible
40
+ given.nil? ? said : { **said, allow_interruptions: given }
41
+ end
42
+
43
+ # When the model may end the call: an empty `when` is whenever it judges.
44
+ def self.hangup_of(ending)
45
+ return { when: "" } if ending == true
46
+ raise DeclarationRefused, NO_ENDING unless ending.is_a?(String) && !ending.strip.empty?
47
+
48
+ { when: ending }
49
+ end
50
+
51
+ # `vendor/model`, or a vendor alone; the model id keeps every slash after the vendor's.
52
+ def self.model_of(named)
53
+ provider, _slash, model = named.to_s.partition("/")
54
+ { provider:, model: }
36
55
  end
37
56
 
38
57
  module Declaring
@@ -45,13 +64,81 @@ module Pinecall
45
64
  end
46
65
  end
47
66
 
48
- # World settings raise at load, naming the command that sets them.
49
- THE_WORLDS.each_key do |field|
50
- define_method(field) do |*_said, **_options|
51
- raise DeclarationRefused, Config.moved_to_the_world(field)
67
+ # The voice, by its vendor and the vendor's own id for it. Declared, it wins over the
68
+ # agent's settings; `builds` and `options` reach the vendor's plugin.
69
+ def voice(provider = NOTHING, voice_id = nil, model: nil, builds: nil, options: nil)
70
+ return environment[:voice] if provider.equal?(NOTHING)
71
+ raise DeclarationRefused, NO_VOICE if voice_id.nil?
72
+
73
+ environment[:voice] = { provider:, voice_id:, model:, builds:, options: }.compact
74
+ end
75
+
76
+ # The model that answers, `vendor/model` or a vendor alone:
77
+ # `llm "openai/gpt-5.4-mini", builds: "responses.LLM", options: { use_websocket: true }`.
78
+ def llm(model = NOTHING, temperature: nil, builds: nil, options: nil)
79
+ return environment[:llm] if model.equal?(NOTHING)
80
+
81
+ environment[:llm] = { **Config.model_of(model), temperature:, builds:, options: }.compact
82
+ end
83
+
84
+ # The ears, `vendor/model` or a vendor alone, and who says the caller's turn is over:
85
+ # `end_of_turn: "stt"` the ears themselves (Deepgram Flux), `"livekit"` or `"smart-turn"`.
86
+ def stt(model = NOTHING, builds: nil, options: nil, end_of_turn: nil)
87
+ return environment[:stt] if model.equal?(NOTHING)
88
+
89
+ ends = end_of_turn&.to_s&.tr("_", "-")
90
+ environment[:stt] = { **Config.model_of(model), builds:, options:, end_of_turn: ends }.compact
91
+ end
92
+
93
+ # How a call opens: the words, said as written, or :improvise for the model's own. The
94
+ # caller cannot cut it short unless `interruptible: true`.
95
+ def greeting(opening = NOTHING, interruptible: nil)
96
+ return environment[:greeting] if opening.equal?(NOTHING)
97
+
98
+ environment[:greeting] = Config.greeting_of(opening, interruptible)
99
+ end
100
+
101
+ # The model opens the call with this instruction: `greeting improvise("…")`.
102
+ def improvise(instruction = "", interruptible: nil) = Improvised.new(instruction, interruptible)
103
+
104
+ # When the model may end the call: in your words, or `true` whenever it judges it done.
105
+ def hangup(ending = NOTHING)
106
+ return environment[:hangup] if ending.equal?(NOTHING)
107
+
108
+ environment[:hangup] = Config.hangup_of(ending)
109
+ end
110
+
111
+ # `language "es"`, `says [{ word: "GSA", spoken: "ge ese a" }]`, `record false`.
112
+ %i[language says record].each do |field|
113
+ define_method(field) do |value = NOTHING|
114
+ return environment[field] if value.equal?(NOTHING)
115
+
116
+ environment[field] = value
117
+ end
118
+ end
119
+
120
+ # The words the ears must know: `hears "Vidal", "Sanitas"`.
121
+ def hears(*words)
122
+ return environment[:hears] if words.empty?
123
+
124
+ environment[:hears] = words.flatten
125
+ end
126
+
127
+ # `turn endpointing_ms: 300`, `docs base: "…", k: 4`, `memory remember: […], forget: […]`,
128
+ # `knowledge path: "…", text: "…"`.
129
+ AS_KEYWORDS.each do |field|
130
+ define_method(field) do |**given|
131
+ return environment[field] if given.empty?
132
+
133
+ environment[field] = given
52
134
  end
53
135
  end
54
136
 
137
+ # What the class declares of its environment, including inherited values.
138
+ def environment
139
+ @environment ||= superclass.respond_to?(:environment) ? superclass.environment.dup : {}
140
+ end
141
+
55
142
  # Set the class docstring explicitly, for classes with no source file.
56
143
  def doc(text = NOTHING)
57
144
  return Doc.for_class(self) if text.equal?(NOTHING)
@@ -84,9 +171,10 @@ module Pinecall
84
171
  @slug = name
85
172
  end
86
173
 
87
- # The AgentConfig sent to the gateway.
174
+ # The AgentConfig sent to the gateway: the contract, and the environment the class declares.
88
175
  def wire_config(tools: nil)
89
176
  {
177
+ **environment,
90
178
  prompt: Prompt::FRAMEWORK,
91
179
  tools:,
92
180
  state_fields: state_field_specs,
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Pinecall
4
4
  # Set by the maintainer at release time.
5
- VERSION = "0.0.4"
5
+ VERSION = "0.0.6"
6
6
  end
@@ -14,12 +14,17 @@ module Pinecall
14
14
  name: { kind: :str },
15
15
  provider: { kind: :str },
16
16
  model: { kind: :str },
17
- voice_id: { kind: :str }
17
+ voice_id: { kind: :str },
18
+ builds: { kind: :str },
19
+ options: { kind: :json }
18
20
  }.freeze,
19
21
  "ModelConfig" => {
20
22
  provider: { kind: :str, required: true },
21
23
  model: { kind: :str, required: true },
22
- temperature: { kind: :float }
24
+ temperature: { kind: :float },
25
+ builds: { kind: :str },
26
+ options: { kind: :json },
27
+ end_of_turn: { kind: :enum, values: %w[stt livekit smart-turn] }
23
28
  }.freeze,
24
29
  "TurnConfig" => {
25
30
  min_interruption_words: { kind: :int },
@@ -18,6 +18,7 @@ module Pinecall
18
18
  }.freeze,
19
19
  "AgentConfig" => {
20
20
  prompt: { kind: :list, items: { kind: :ref, ref: "PromptBlockSpec" } },
21
+ language: { kind: :str },
21
22
  greeting: { kind: :ref, ref: "GreetingConfig" },
22
23
  voice: { kind: :ref, ref: "VoiceConfig" },
23
24
  llm: { kind: :ref, ref: "ModelConfig" },
data/sig/pinecall.rbs CHANGED
@@ -84,18 +84,22 @@ module Pinecall
84
84
  def self.panel: (?String) { (Panel::Who) -> void } -> Panel::Declared
85
85
  def self.declared_panel: () -> Panel::Declared?
86
86
 
87
- # the world's, refused at load with the verb that sets each
88
- def self.voice: (*untyped, **untyped) -> bot
89
- def self.llm: (*untyped, **untyped) -> bot
90
- def self.stt: (*untyped, **untyped) -> bot
91
- def self.greeting: (*untyped, **untyped) -> bot
92
- def self.hangup: (*untyped, **untyped) -> bot
93
- def self.says: (*untyped, **untyped) -> bot
94
- def self.hears: (*untyped, **untyped) -> bot
95
- def self.memory: (*untyped, **untyped) -> bot
96
- def self.record: (*untyped, **untyped) -> bot
97
- def self.knowledge: (*untyped, **untyped) -> bot
98
- def self.docs: (*untyped, **untyped) -> bot
87
+ # the environment the class declares, each over the agent's settings
88
+ def self.voice: (?String, ?String?, ?model: String?, ?builds: String?, ?options: Hash[untyped, untyped]?) -> untyped
89
+ def self.llm: (?String, ?temperature: Float?, ?builds: String?, ?options: Hash[untyped, untyped]?) -> untyped
90
+ def self.stt: (?String, ?builds: String?, ?options: Hash[untyped, untyped]?, ?end_of_turn: (String | Symbol)?) -> untyped
91
+ def self.language: (?String) -> untyped
92
+ def self.says: (?Array[Hash[Symbol, String]]) -> untyped
93
+ def self.record: (?bool) -> untyped
94
+ def self.hears: (*String | Array[String]) -> untyped
95
+ def self.greeting: (?(String | Symbol | untyped), ?interruptible: bool?) -> untyped
96
+ def self.improvise: (?String, ?interruptible: bool?) -> untyped
97
+ def self.hangup: (?(String | true)) -> untyped
98
+ def self.turn: (**untyped) -> untyped
99
+ def self.knowledge: (**untyped) -> untyped
100
+ def self.docs: (**untyped) -> untyped
101
+ def self.memory: (**untyped) -> untyped
102
+ def self.environment: () -> Hash[Symbol, untyped]
99
103
 
100
104
  def self.doc: (?String) -> String?
101
105
  def self.slug: (?String) -> String
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pinecall
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pinecall