nitro_intelligence 3.0.0 → 3.0.1

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: a6f2f9a233086c2c5b63e91124dd8ff9f6a838ba3eb1a7e08b382029efaf6c8a
4
- data.tar.gz: d4b72f0693e8c10ad97d6ead9eebb469f69043d1323e49c097811530e17c5d90
3
+ metadata.gz: 7653522a1c44214cfec271be7416e5b2520df8411cc59fb729bbd4fe9c09a9ea
4
+ data.tar.gz: 626d6c64b382520afe8a01f6ab4d9e8d622fd4abd671ec53c0a355a2668c8ff2
5
5
  SHA512:
6
- metadata.gz: caf47b70030388d5a3aaaa31e4fa9aa217b401e740fdd41a166539c12bfae0e9c4342e57543510a83af4c8c9788e7c50b752324273ee4a7302c903f754a5b14e
7
- data.tar.gz: 426c37a3551965cee7946631d27b00b9e84e79620640cf1575a60d827a15f5e3b2ae1f40cde95e2df39efa15ec6c2a730ee3f66fbf1dcb4e4c0c148c037e5be2
6
+ metadata.gz: d2b91cce36b7524319ebc37b126424d0c63da854fc4da6bd8678d9519c8301d72166c6fe70c93e808809e3eba0c1b7926fa99ac5666bbf54981e7b49a0666fbd
7
+ data.tar.gz: cd1399a043ba90a041264693463d21fd1957e94dcf7da2c6adb9487a31ae8b9fce62af0ba0abfed2533118614c2133f15120e0bf12fc80a93f6d7f168b4ba4ee
data/docs/README.md CHANGED
@@ -424,7 +424,62 @@ trace_id = NitroIntelligence::Trace.create_id(seed: document_id)
424
424
  reporter.score(name: "precision", value: 0.5, trace_id:)
425
425
  ```
426
426
 
427
- ### Prompt Variables and Config
427
+ ### Prompts
428
+
429
+ #### Message Shape
430
+
431
+ A chat completion needs a user message in order for the model to answer. What you have to supply depends on the type of the prompt you name, and the two types behave differently.
432
+
433
+ ##### Text prompts
434
+
435
+ A text prompt in Cerebro is equivalent to the system prompt. System prompts are used to pre-load initial context into the model needed for following call executions.
436
+
437
+ Pure text prompts require a user message to be supplied. For example:
438
+
439
+ ```ruby
440
+ # Wrong - Raises ObservedChatPromptError - A prompt alone, provides a system message alone with nothing to answer
441
+ client.chat(parameters: { prompt_name: "Assistant" })
442
+
443
+ # Correct - The prompt supplies the system message, you supply the user message
444
+ client.chat(message: "Why is the sky blue?", parameters: { prompt_name: "Assistant" })
445
+ ```
446
+
447
+ ##### Chat prompts
448
+
449
+ A chat prompt in Cerebro holds a list of role-tagged messages rather than a single string, so it can carry its own user turn. Define one when the prompt structure should represent a conversation flow vs. a single block of static text, e.g. pre-modeling an interaction you want to continue:
450
+
451
+ ```yaml
452
+ [
453
+ {
454
+ "role": "system",
455
+ "content": "You are a helpful support agent for an online bookstore. Be concise and friendly. Our return window is 30 days."
456
+ },
457
+ {
458
+ "role": "user",
459
+ "content": "Hi, do you ship to Canada?"
460
+ },
461
+ {
462
+ "role": "assistant",
463
+ "content": "Yes! We ship to Canada. Delivery usually takes 5–8 business days."
464
+ },
465
+ {
466
+ "role": "user",
467
+ "content": "{{question}}"
468
+ }
469
+ ]
470
+ ```
471
+
472
+ That prompt is self-sufficient, so no additional `message:` is needed:
473
+
474
+ ```ruby
475
+ client.chat(parameters: { prompt_name: "Appointment Extractor", prompt_variables: { document: text } })
476
+ ```
477
+
478
+ Any messages you do pass are appended after the prompt's own system message, so a chat prompt can also serve as a preamble to a live conversation.
479
+
480
+ A chat prompt containing only a system message has the same problem as a text prompt, and is refused in the same way. If you find yourself writing one, either add a user message to it in Cerebro or make it a text prompt and pass the turn from the caller via the `message` keyword.
481
+
482
+ #### Prompt Variables and Config
428
483
 
429
484
  Prompts are often created with "variables". These variables can be supplied and compiled into the prompt. For example:
430
485
 
@@ -5,6 +5,8 @@ module NitroIntelligence
5
5
  module Handlers
6
6
  module Observed
7
7
  class ChatHandler
8
+ class ObservedChatPromptError < StandardError; end
9
+
8
10
  def initialize(base_handler:, observer:)
9
11
  @base_handler = base_handler
10
12
  @observer = observer
@@ -14,6 +16,8 @@ module NitroIntelligence
14
16
  @base_handler.validate_and_resolve!(parameters, message)
15
17
 
16
18
  prompt = handle_prompt(parameters:)
19
+ validate_message_shape!(parameters:, prompt:)
20
+
17
21
  trace_name = parameters[:trace_name] || prompt&.name || @observer.project_client.project.slug
18
22
 
19
23
  @observer.observe(
@@ -30,6 +34,46 @@ module NitroIntelligence
30
34
 
31
35
  private
32
36
 
37
+ # A chat completion needs a turn for the model to answer, and it is the model's own
38
+ # chat template that insists on one: Qwen's raises "No user query found in
39
+ # messages." Left to the gateway that costs a round trip and comes back as a 400
40
+ # whose body the caller cannot read, so it is caught here instead, while the prompt
41
+ # is still in hand and the advice can name what to do about it.
42
+ #
43
+ # Presence of the turn is what is checked, not its usefulness. A template may well
44
+ # accept an empty user message -- Qwen's does -- but a caller who sent one almost
45
+ # never meant to, so blank content is refused rather than forwarded. Content
46
+ # arriving as an array of parts is taken at face value, since a message carrying
47
+ # only an image is a legitimate turn.
48
+ def validate_message_shape!(parameters:, prompt:)
49
+ return if parameters[:messages].any? { |message| user_turn?(message) }
50
+
51
+ raise ObservedChatPromptError, missing_user_message_error(prompt)
52
+ end
53
+
54
+ def user_turn?(message)
55
+ role = message[:role] || message["role"]
56
+ return false unless role.to_s == "user"
57
+
58
+ (message[:content] || message["content"]).present?
59
+ end
60
+
61
+ def missing_user_message_error(prompt)
62
+ case prompt&.type
63
+ when "text"
64
+ "The prompt #{prompt.name.inspect} is a text prompt, so it contributes only a system message and " \
65
+ "this request carries no turn for the model to answer. Pass a `message:`, or define " \
66
+ "#{prompt.name.inspect} as a chat prompt in Cerebro so that it carries its own user message."
67
+ when "chat"
68
+ "The chat prompt #{prompt.name.inspect} contains no user message and none was supplied, so this " \
69
+ "request carries no turn for the model to answer. Add a user message to #{prompt.name.inspect} " \
70
+ "in Cerebro, or pass a `message:`."
71
+ else
72
+ "This request carries no user message, so there is no turn for the model to answer. Pass a " \
73
+ "`message:`, or supply `parameters[:messages]` including a message with the `user` role."
74
+ end
75
+ end
76
+
33
77
  def handle_prompt(parameters:)
34
78
  prompt = NitroIntelligence::Observability::PromptResolver.for(
35
79
  store: @observer.project_client.project.prompt_store,
@@ -1,3 +1,3 @@
1
1
  module NitroIntelligence
2
- VERSION = "3.0.0".freeze
2
+ VERSION = "3.0.1".freeze
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nitro_intelligence
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0
4
+ version: 3.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Igor Artemenko