Module: Legion::LLM::DaemonClient
- Extended by:
- Legion::Logging::Helper
- Defined in:
- lib/legion/llm/daemon_client.rb
Constant Summary collapse
- HEALTH_CACHE_TTL =
30- DEFAULT_TIMEOUT =
60
Class Method Summary collapse
-
.available? ⇒ Boolean
Returns true if the daemon is reachable and healthy.
-
.chat(message:, request_id: nil, context: {}, tier_preference: :auto, model: nil, provider: nil) ⇒ Object
POSTs a chat request to the daemon REST API.
-
.check_health ⇒ Object
GETs /api/health.
-
.daemon_url ⇒ Object
Returns the daemon URL from settings, cached after first read.
-
.http_get(path) ⇒ Object
Builds and sends a GET request.
-
.http_post(path, body, timeout: DEFAULT_TIMEOUT) ⇒ Object
Builds and sends a POST request with a JSON body.
-
.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.
-
.interpret_response(response) ⇒ Object
Maps an HTTP response to a status hash.
-
.mark_unhealthy ⇒ Object
Marks the daemon as unhealthy and records the timestamp.
-
.reset! ⇒ Object
Clears all cached state.
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.
23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
# File 'lib/legion/llm/daemon_client.rb', line 23 def available? return false if daemon_url.nil? now = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) return true if @healthy == true && @health_checked_at && (now - @health_checked_at) < HEALTH_CACHE_TTL result = check_health if result @healthy = true @health_checked_at = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) end 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.
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
# File 'lib/legion/llm/daemon_client.rb', line 40 def chat(message:, request_id: nil, context: {}, tier_preference: :auto, model: nil, provider: nil) request_id ||= SecureRandom.uuid body = { 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. } end |
.check_health ⇒ Object
GETs /api/health. Returns true on 200, false otherwise. Updates @healthy and @health_checked_at.
78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/legion/llm/daemon_client.rb', line 78 def check_health response = http_get('/api/health') healthy = response.code == '200' @healthy = healthy @health_checked_at = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) 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_url ⇒ Object
Returns the daemon URL from settings, cached after first read. Returns nil if settings are unavailable or the key is missing.
62 63 64 65 66 |
# File 'lib/legion/llm/daemon_client.rb', line 62 def daemon_url return @daemon_url if defined?(@daemon_url) @daemon_url = fetch_daemon_url end |
.http_get(path) ⇒ Object
Builds and sends a GET request. Returns Net::HTTPResponse.
99 100 101 102 103 104 105 106 107 |
# File 'lib/legion/llm/daemon_client.rb', line 99 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.
131 132 133 134 135 136 137 138 139 140 |
# File 'lib/legion/llm/daemon_client.rb', line 131 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 = ::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.
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 |
# File 'lib/legion/llm/daemon_client.rb', line 112 def inference(messages:, tools: [], model: nil, provider: nil, caller: nil, conversation_id: nil, timeout: 120) body = { 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. } end |
.interpret_response(response) ⇒ Object
Maps an HTTP response to a status hash. Follows the Legion API format: { data: … } for success, { error: … } for failure.
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 |
# File 'lib/legion/llm/daemon_client.rb', line 145 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_unhealthy ⇒ Object
Marks the daemon as unhealthy and records the timestamp.
92 93 94 95 96 |
# File 'lib/legion/llm/daemon_client.rb', line 92 def mark_unhealthy log.warn("Daemon marked unhealthy url=#{daemon_url}") @healthy = false @health_checked_at = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) end |
.reset! ⇒ Object
Clears all cached state. Returns self for chaining.
69 70 71 72 73 74 |
# File 'lib/legion/llm/daemon_client.rb', line 69 def reset! remove_instance_variable(:@daemon_url) if defined?(@daemon_url) @healthy = nil @health_checked_at = nil self end |