Class: NitroIntelligence::Assistants

Inherits:
Object
  • Object
show all
Defined in:
lib/nitro_intelligence/assistants.rb

Defined Under Namespace

Classes: ConfigurationError, RunError, ThreadInitializationError, ThreadResumptionError, ThreadStateError

Constant Summary collapse

THREAD_CONFLICT_CODE =

Assistants answers with a conflict when ifExists: "raise" is sent for a thread that already exists.

409
DEFAULT_BASE_URL =

Every assistant this gem is built for is served by the same deployment, so a client told nothing about where to reach one gets that deployment. A host talking to a different one -- a review environment, a local server -- still says so.

"https://assistants.powerhome.ai".freeze
DEFAULT_USER_ID =
"default-user".freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(api_key:, base_url: nil, user_id: DEFAULT_USER_ID) ⇒ Assistants

Returns a new instance of Assistants.

Raises:



27
28
29
30
31
32
33
34
35
36
# File 'lib/nitro_intelligence/assistants.rb', line 27

def initialize(api_key:, base_url: nil, user_id: DEFAULT_USER_ID)
  raise ConfigurationError, "api_key is required" if api_key.blank?
  raise ConfigurationError, "user_id is required" if user_id.blank?

  @base_url = base_url.presence || DEFAULT_BASE_URL
  @api_key = api_key
  @user_id = user_id
  @tool_call_review_validator = ToolCallReviewValidator.new
  @graph_ids = {}
end

Instance Attribute Details

#base_url ⇒ Object (readonly)

Returns the value of attribute base_url.



25
26
27
# File 'lib/nitro_intelligence/assistants.rb', line 25

def base_url
  @base_url
end

#user_id ⇒ Object (readonly)

Returns the value of attribute user_id.



25
26
27
# File 'lib/nitro_intelligence/assistants.rb', line 25

def user_id
  @user_id
end

Instance Method Details

#await_run(thread_id:, assistant_id:, messages:, context: {}) ⇒ Object

Raises:



38
39
40
41
42
43
44
45
46
# File 'lib/nitro_intelligence/assistants.rb', line 38

def await_run(thread_id:, assistant_id:, messages:, context: {})
  raise RunError, "messages cannot be empty" if messages.blank?

  initial_state = messages[0..-2]
  last_message = messages.last

  initialize_thread_if_needed(thread_id:, assistant_id:, initial_state:)
  trigger_run(thread_id:, assistant_id:, context:, last_message:)
end

#review_tool_calls(thread_id:, assistant_id:, tool_calls:, context: {}) ⇒ Object

Resumes an interrupted thread with one review per tool call the interrupt is holding. Each review is keyed by tool call id and names an action -- approve, edit, reject or respond -- from the decisions the interrupt allows for that tool. edit carries args, merged over the arguments the model asked for; respond carries the message returned to the model as the tool's result; reject may carry a message explaining the refusal.

Assistants records nothing about who reviewed a tool call, and the resume payload it accepts has nowhere to carry it, so there is no reviewer argument to pass.



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/nitro_intelligence/assistants.rb', line 98

def review_tool_calls(thread_id:, assistant_id:, tool_calls:, context: {})
  thread = get_thread(thread_id:)
  raise ThreadResumptionError, "Thread #{thread_id} is not in the interrupted state" unless interrupted?(thread)

  interrupt = ToolCallReviewInterrupt.new(get_thread_state(thread_id:))
  tool_calls_under_review = interrupt.tool_calls

  if tool_calls_under_review.empty?
    raise ThreadResumptionError, "Thread #{thread_id} has no tool calls awaiting review"
  end

  @tool_call_review_validator.validate!(tool_calls:, tool_calls_under_review:)

  resume_run(
    thread_id:,
    assistant_id:,
    resume: { decisions: interrupt.decisions(tool_calls) },
    context:
  )

  nil
end

#thread_messages(thread_id:) ⇒ Object

The thread's messages as Assistants reports them, unformatted, oldest first. Each message carries its own type ("human", "ai", "tool", ...), which callers map to their own roles.



56
57
58
# File 'lib/nitro_intelligence/assistants.rb', line 56

def thread_messages(thread_id:)
  messages_in(thread_state(thread_id:))
end

#thread_state(thread_id:) ⇒ Object

The thread's state as Assistants reports it, unformatted. Callers that only want the conversation should reach for #thread_messages instead.



50
51
52
# File 'lib/nitro_intelligence/assistants.rb', line 50

def thread_state(thread_id:)
  get_thread_state(thread_id:, error: ThreadStateError)
end

#tool_calls_pending_review(thread_id:) ⇒ Object



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/nitro_intelligence/assistants.rb', line 60

def tool_calls_pending_review(thread_id:)
  thread_state = get_thread_state(thread_id:)
  messages = messages_in(thread_state)
  reviewed_tool_call_ids = tool_messages(messages).map { |message| message["tool_call_id"] }

  messages.each_with_index.flat_map do |message, index|
    next [] unless message["type"] == "ai"

    pending_tool_calls(message, reviewed_tool_call_ids).map do |tool_call|
      {
        "previous_message_id" => index.zero? ? nil : messages[index - 1]&.dig("id"),
        "id" => tool_call["id"],
        "name" => tool_call["name"],
        "args" => tool_call["args"] || {},
      }
    end
  end
end

#tool_calls_under_review(thread_id:) ⇒ Object

The tool calls the thread's interrupt is holding, in the order the platform wants decisions for them, each with the allowed_decisions a reviewer may take on it. Empty when the thread is not waiting on a review.

A tool the assistant is not configured to interrupt on runs without review, so an AI message can mix calls under review with calls that are only waiting to be executed. This reports the former; #tool_calls_pending_review reports both.



86
87
88
# File 'lib/nitro_intelligence/assistants.rb', line 86

def tool_calls_under_review(thread_id:)
  ToolCallReviewInterrupt.new(get_thread_state(thread_id:)).tool_calls
end