Module: Legion::LLM::Call::DaemonClient

Extended by:
Legion::Logging::Helper
Defined in:
lib/legion/llm/call/daemon_client.rb

Constant Summary collapse

HEALTH_CACHE_TTL =
30
DEFAULT_TIMEOUT =
60

Class Method Summary collapse

Class Method Details

.available?Boolean

Returns true if the daemon is reachable and healthy. Returns false immediately if daemon_url is nil. Caches a positive health check for HEALTH_CACHE_TTL seconds. An unhealthy result is not cached — rechecks on every call.

Returns:

  • (Boolean)


28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/legion/llm/call/daemon_client.rb', line 28

def available?
  return false if daemon_url.nil?

  now = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
  cached_healthy = state_mutex.synchronize do
    @healthy == true && @health_checked_at && (now - @health_checked_at) < HEALTH_CACHE_TTL
  end
  return true if cached_healthy

  result = check_health
  record_health(result) if result
  result
end

.chat(message:, request_id: nil, context: {}, tier_preference: :auto, model: nil, provider: nil) ⇒ Object

POSTs a chat request to the daemon REST API. Returns a status hash based on the HTTP response code.



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/legion/llm/call/daemon_client.rb', line 44

def chat(message:, request_id: nil, context: {}, tier_preference: :auto, model: nil, provider: nil)
  request_id ||= SecureRandom.uuid

  body = {
    message:         message,
    request_id:      request_id,
    context:         context,
    tier_preference: tier_preference
  }
  body[:model]    = model    if model
  body[:provider] = provider if provider

  response = http_post('/api/llm/chat', body)
  interpret_response(response)
rescue StandardError => e
  handle_exception(e, level: :warn, operation: 'llm.daemon_client.chat', request_id: request_id)
  mark_unhealthy
  { status: :unavailable, error: e.message }
end

.check_healthObject

GETs /api/health. Returns true on 200, false otherwise. Updates @healthy and @health_checked_at.



86
87
88
89
90
91
92
93
94
95
96
# File 'lib/legion/llm/call/daemon_client.rb', line 86

def check_health
  response = http_get('/api/health')
  healthy = response.code == '200'
  record_health(healthy)
  log.info("Daemon health check result=#{healthy ? 'healthy' : 'unhealthy'} url=#{daemon_url}")
  healthy
rescue StandardError => e
  handle_exception(e, level: :warn)
  mark_unhealthy
  false
end

.daemon_urlObject

Returns the daemon URL from settings, cached after first read. Returns nil if settings are unavailable or the key is missing.



66
67
68
69
70
71
72
# File 'lib/legion/llm/call/daemon_client.rb', line 66

def daemon_url
  state_mutex.synchronize do
    return @daemon_url if defined?(@daemon_url)

    @daemon_url = fetch_daemon_url
  end
end

.http_get(path) ⇒ Object

Builds and sends a GET request. Returns Net::HTTPResponse.



105
106
107
108
109
110
111
112
113
# File 'lib/legion/llm/call/daemon_client.rb', line 105

def http_get(path)
  uri     = URI.parse("#{daemon_url}#{path}")
  http    = Net::HTTP.new(uri.host, uri.port)
  http.open_timeout = 2
  http.read_timeout = 2
  request = Net::HTTP::Get.new(uri.request_uri)
  request['Content-Type'] = 'application/json'
  http.request(request)
end

.http_post(path, body, timeout: DEFAULT_TIMEOUT) ⇒ Object

Builds and sends a POST request with a JSON body. Returns Net::HTTPResponse. The optional timeout: keyword overrides the default read timeout.



137
138
139
140
141
142
143
144
145
146
# File 'lib/legion/llm/call/daemon_client.rb', line 137

def http_post(path, body, timeout: DEFAULT_TIMEOUT)
  uri     = URI.parse("#{daemon_url}#{path}")
  http    = Net::HTTP.new(uri.host, uri.port)
  http.open_timeout = 5
  http.read_timeout = timeout
  request = Net::HTTP::Post.new(uri.request_uri)
  request['Content-Type'] = 'application/json'
  request.body = Legion::JSON.dump(body)
  http.request(request)
end

.inference(messages:, tools: [], model: nil, provider: nil, caller: nil, conversation_id: nil, timeout: 120) ⇒ Object

POSTs a conversation-level inference request to the daemon REST API. Accepts a full messages array and optional tool schemas. Returns a status hash with structured inference fields on success.



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/legion/llm/call/daemon_client.rb', line 118

def inference(messages:, tools: [], model: nil, provider: nil, caller: nil, conversation_id: nil,
              timeout: 120)
  body = { messages: messages, tools: tools }
  body[:model]           = model           if model
  body[:provider]        = provider        if provider
  body[:caller]          = caller          if caller
  body[:conversation_id] = conversation_id if conversation_id

  response = http_post('/api/llm/inference', body, timeout: timeout)
  interpret_inference_response(response)
rescue StandardError => e
  handle_exception(e, level: :warn, operation: 'llm.daemon_client.inference', conversation_id: conversation_id)
  mark_unhealthy
  { status: :unavailable, error: e.message }
end

.interpret_response(response) ⇒ Object

Maps an HTTP response to a status hash. Follows the Legion API format: { data: … } for success, { error: … } for failure.



151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/legion/llm/call/daemon_client.rb', line 151

def interpret_response(response)
  code   = response.code.to_i
  parsed = safe_parse(response.body)

  case code
  when 200
    { status: :immediate, body: parsed.fetch(:data, parsed) }
  when 201
    { status: :created,   body: parsed.fetch(:data, parsed) }
  when 202
    data = parsed.fetch(:data, {})
    { status: :accepted, request_id: data[:request_id], poll_key: data[:poll_key] }
  when 403
    log.warn("Daemon returned 403 Denied url=#{daemon_url}")
    { status: :denied, error: parsed.fetch(:error, parsed) }
  when 429
    retry_after = extract_retry_after(response, parsed)
    log.warn("Daemon returned 429 RateLimited url=#{daemon_url} retry_after=#{retry_after}")
    { status: :rate_limited, retry_after: retry_after }
  when 503
    { status: :unavailable }
  else
    { status: :error, code: code, body: parsed }
  end
end

.mark_unhealthyObject

Marks the daemon as unhealthy and records the timestamp.



99
100
101
102
# File 'lib/legion/llm/call/daemon_client.rb', line 99

def mark_unhealthy
  log.warn("Daemon marked unhealthy url=#{daemon_url}")
  record_health(false)
end

.reset!Object

Clears all cached state. Returns self for chaining.



75
76
77
78
79
80
81
82
# File 'lib/legion/llm/call/daemon_client.rb', line 75

def reset!
  state_mutex.synchronize do
    remove_instance_variable(:@daemon_url) if defined?(@daemon_url)
    @healthy           = nil
    @health_checked_at = nil
  end
  self
end