npc-clients-godot/Client.gd

951 lines
27 KiB
GDScript

extends Node
## GumYum Game Client for Godot (API Key Authentication Only)
##
## Streamlined client for game integration using API keys only.
## Perfect for embedding in shipped games with just the essential features.
##
## This client does NOT support username/password authentication.
## Use API keys obtained from the GumYum dashboard.
##
## Features:
## - Character spawning and dialogue
## - API key authentication only (no login/register)
## - Automatic JWT token management via API key exchange
## - Automatic retry and error handling
## - Streaming dialogue support
signal character_spawned(character: GumYumNPC)
signal dialogue_received(response: String, context: Dictionary, mood_transition: Dictionary)
signal dialogue_chunk_received(chunk: String)
signal dialogue_stream_started
signal dialogue_stream_ended(
full_response: String, context: Dictionary, mood_transition: Dictionary
)
signal request_completed(endpoint: String, data: Dictionary)
signal request_failed(endpoint: String, error: String)
## Manager Classes
class NPCManager:
var client: Node
func _init(c: Node) -> void:
client = c
## Spawn a character - returns GumYumNPC object
## Pass null for npc_id to spawn a random NPC
func spawn(
universe_id: String, seed: int, npc_id = null, callback: Callable = Callable()
) -> void:
var params = {"universe_id": universe_id, "seed": seed}
var endpoint: String
if npc_id != null:
# Spawn specific NPC
params["npc_id"] = npc_id
endpoint = "npc"
else:
# Spawn random NPC - let API handle the randomness
endpoint = "npc/spawn"
# Make the API request
client.request(
HTTPClient.METHOD_GET,
endpoint,
{},
params,
func(response):
# Check if this is a redirect response
if response.has("redirect_url"):
# Follow the redirect to get actual NPC data
var redirect_path = response.redirect_url
# Strip the /v1/ prefix since client adds it
if redirect_path.begins_with("/v1/"):
redirect_path = redirect_path.substr(4)
# Make the redirect request
client.request(
HTTPClient.METHOD_GET,
redirect_path,
{},
{},
func(npc_data):
if not npc_data.has("error"):
# Create NPC object with client reference
var npc = GumYumNPC.new(npc_data, client)
npc._connect_client_signals()
client.character_spawned.emit(npc)
if callback.is_valid():
var npc_obj = (
GumYumNPC.new(npc_data, client)
if not npc_data.has("error")
else null
)
if npc_obj:
npc_obj._connect_client_signals()
callback.call(npc_obj if npc_obj else npc_data)
)
else:
# Direct response (shouldn't happen with /npc/spawn)
if not response.has("error"):
var npc = GumYumNPC.new(response, client)
npc._connect_client_signals()
client.character_spawned.emit(npc)
if callback.is_valid():
callback.call(npc)
)
## Spawn with advanced filters - supports any NPC field
## Examples:
## spawn_filtered("kingdom", 12345, {"profession": ["warrior", "knight"], "location": ["barracks", "castle"]})
## spawn_filtered("blade-runner", 67890, {"personality_type": [8], "sex": ["female"]})
func spawn_filtered(
universe_id: String, seed: int, filters: Dictionary = {}, callback: Callable = Callable()
) -> void:
var data = {"universe_id": universe_id, "world_seed": seed, "filters": filters}
var endpoint = "npc/spawn_filtered"
# spawn_filtered uses POST with data in body
client.request(
HTTPClient.METHOD_POST,
endpoint,
data,
{},
func(response):
# Check if this is a redirect response
if response.has("redirect_url"):
# Follow the redirect to get actual NPC data
var redirect_path = response.redirect_url
# Strip the /v1/ prefix since client adds it
if redirect_path.begins_with("/v1/"):
redirect_path = redirect_path.substr(4)
# Make the redirect request
client.request(
HTTPClient.METHOD_GET,
redirect_path,
{},
{},
func(npc_data):
if not npc_data.has("error"):
# Create NPC object with client reference
var npc = GumYumNPC.new(npc_data, client)
npc._connect_client_signals()
client.character_spawned.emit(npc)
if callback.is_valid():
var npc_obj = (
GumYumNPC.new(npc_data, client)
if not npc_data.has("error")
else null
)
if npc_obj:
npc_obj._connect_client_signals()
callback.call(npc_obj if npc_obj else npc_data)
)
else:
# Direct response (shouldn't happen with /npc/spawn)
if not response.has("error"):
var npc = GumYumNPC.new(response, client)
npc._connect_client_signals()
client.character_spawned.emit(npc)
if callback.is_valid():
callback.call(npc)
else:
if callback.is_valid():
callback.call(response)
)
class UniverseManager:
var client: Node
func _init(c: Node) -> void:
client = c
## List public universes (works with API keys)
func list_public(callback: Callable) -> void:
client.request(HTTPClient.METHOD_GET, "public/universes", {}, {}, callback)
## List user's universes (works with JWT from API key exchange)
func list(callback: Callable) -> void:
client.request(HTTPClient.METHOD_GET, "universes", {}, {}, callback)
func copy(
_public_universe_id: String, _custom_name: String = "", callback: Callable = Callable()
) -> void:
push_error(
"[GumYum] copy() requires user authentication. Use API keys with a known universe ID instead."
)
if callback.is_valid():
callback.call(
{"error": "Authentication required. Use API keys with a known universe ID."}
)
func get_universe(universe_id: String, callback: Callable) -> void:
client.request(HTTPClient.METHOD_GET, "universes/" + universe_id, {}, {}, callback)
class ChatManager:
var client: Node
func _init(c: Node) -> void:
client = c
## Generate dialogue (OpenAI-compatible chat.completions)
func completions(
messages: Array,
npc_params: Dictionary,
temperature: float = 0.8,
callback: Callable = Callable(),
max_tokens: int = 2000,
stream: bool = false
) -> void:
var data = {
"model": "gumyum-npc",
"temperature": temperature,
"messages": messages,
"stream": stream,
"npc_params": npc_params,
"max_tokens": max_tokens
}
# Extract npc_id to top level if it exists in npc_params
if npc_params.has("npc_id"):
data["npc_id"] = npc_params["npc_id"]
if stream:
# Use streaming method
client._request_stream(HTTPClient.METHOD_POST, "chat/completions", data, {}, callback)
else:
client.request(
HTTPClient.METHOD_POST,
"chat/completions",
data,
{},
func(response):
if (
not response.has("error")
and response.has("choices")
and response.choices.size() > 0
):
var content = response.choices[0].message.content
var context = response.get("npc_context", {})
var mood_transition = response.get("mood_transition", {})
client.dialogue_received.emit(content, context, mood_transition)
if callback.is_valid():
callback.call(response)
)
## Generate dialogue (legacy method name for backwards compatibility)
func chat(
messages: Array,
npc_params: Dictionary,
temperature: float = 0.8,
callback: Callable = Callable(),
max_tokens: int = 2000
) -> void:
completions(messages, npc_params, temperature, callback, max_tokens, false)
## Generate dialogue with streaming support
func chat_stream(
messages: Array,
npc_params: Dictionary,
temperature: float = 0.8,
callback: Callable = Callable(),
max_tokens: int = 2000
) -> void:
var data = {
"model": "gumyum-npc",
"temperature": temperature,
"messages": messages,
"stream": true, # Enable streaming
"npc_params": npc_params,
"max_tokens": max_tokens
}
# Extract npc_id to top level if it exists in npc_params
if npc_params.has("npc_id"):
data["npc_id"] = npc_params["npc_id"]
# Use the streaming request method
client._request_stream(HTTPClient.METHOD_POST, "chat/completions", data, {}, callback)
## Configuration
@export var base_url: String = "https://npc.gumyum.com"
@export var api_version: String = "v1"
@export var timeout: float = 75.0 # Higher than server's 65s timeout
@export var max_retries: int = 3
@export var debug_mode: bool = false
## API Credentials
var _api_key: String = "" # The public API key
var _api_secret: String = "" # The secret key
var _jwt_token: String = ""
var _refresh_token: String = ""
var _token_expires_at: float = 0.0
## Set API credentials - matches Python client interface
var api_key: String:
set(value):
_api_key = value
if debug_mode:
print("[GumYum Game] API key set: ", value)
get:
return _api_key
var api_secret: String:
set(value):
_api_secret = value
if debug_mode:
print("[GumYum Game] API secret set: ****")
get:
return _api_secret
## HTTP Clients for parallel requests
var _http_clients: Array = [] # Pool of HTTP clients
var _max_parallel_requests: int = 5 # Max concurrent requests
var _pending_requests: Array = []
var _active_requests: int = 0
var _auth_in_progress: bool = false # Prevent concurrent auth requests
## Managers (initialized in _ready)
var npcs: NPCManager
var universes: UniverseManager
var chat: ChatManager
func _ready() -> void:
_setup_http_client()
_initialize_managers()
# No need to load auth for API key-only authentication
if debug_mode:
print("[GumYum Game] Client initialized")
func _setup_http_client() -> void:
# Create a pool of HTTP clients for parallel requests
for i in range(_max_parallel_requests):
var http_client = HTTPRequest.new()
http_client.name = "HTTPClient_" + str(i)
add_child(http_client)
http_client.timeout = timeout
http_client.request_completed.connect(_on_request_completed.bind(http_client))
_http_clients.append(http_client)
func _initialize_managers() -> void:
npcs = NPCManager.new(self)
universes = UniverseManager.new(self)
chat = ChatManager.new(self)
## Check if authenticated
func is_ready() -> bool:
return _jwt_token != "" and Time.get_ticks_msec() / 1000.0 < _token_expires_at
## Check if token needs refresh (within 60 seconds of expiry)
func _needs_refresh() -> bool:
return _jwt_token != "" and Time.get_ticks_msec() / 1000.0 > (_token_expires_at - 60)
## Get full API URL
func get_api_url(endpoint: String) -> String:
return "%s/%s/%s" % [base_url, api_version, endpoint.lstrip("/")]
## Get request headers
func get_headers() -> PackedStringArray:
var headers = PackedStringArray()
headers.append("Content-Type: application/json")
headers.append("User-Agent: GumYum-Godot-Game-SDK/1.0.0")
# Add API key header if we have credentials
if _api_key and _api_secret:
headers.append("X-API-Key: " + _api_key + ":" + _api_secret)
if _jwt_token:
headers.append("Authorization: Bearer " + _jwt_token)
return headers
## Make API request
func request(
method: HTTPClient.Method,
endpoint: String,
data: Dictionary = {},
query_params: Dictionary = {},
callback: Callable = Callable()
) -> void:
# Queue the request
var request_data = {
"method": method,
"endpoint": endpoint,
"data": data,
"query_params": query_params,
"callback": callback,
"retries": 0
}
_pending_requests.append(request_data)
_process_request_queue()
func _process_request_queue() -> void:
# Process multiple requests in parallel up to the limit
while _active_requests < _max_parallel_requests and _pending_requests.size() > 0:
# Check if we need to exchange API key first
if _api_key != "" and _api_secret != "" and not is_ready():
_exchange_api_key_for_jwt()
return
# Check if we need to refresh token
if _needs_refresh() and _refresh_token != "":
_refresh_jwt_token()
return
# Find an available HTTP client
var available_client: HTTPRequest = null
for client in _http_clients:
if client.get_meta("busy", false) == false:
available_client = client
break
if available_client:
var request = _pending_requests.pop_front()
available_client.set_meta("busy", true)
available_client.set_meta("request_data", request)
_active_requests += 1
_execute_request(request, available_client)
func _execute_request(request: Dictionary, http_client: HTTPRequest) -> void:
var url = get_api_url(request.endpoint)
# Add query parameters
if request.query_params.size() > 0:
var query_parts = []
for key in request.query_params:
query_parts.append(key + "=" + str(request.query_params[key]).uri_encode())
url += "?" + "&".join(query_parts)
var headers = get_headers()
var body = ""
if request.method in [HTTPClient.METHOD_POST, HTTPClient.METHOD_PUT]:
body = JSON.stringify(request.data)
if debug_mode:
print("[GumYum Game] %s %s" % [request.method, request.endpoint])
print("[GumYum Game] Full URL: %s" % url)
var error = http_client.request_raw(
url, headers, request.method, body.to_utf8_buffer() if body else PackedByteArray()
)
if error != OK:
_handle_request_error(request, "Failed to make request: " + error_string(error))
http_client.set_meta("busy", false)
_active_requests -= 1
_process_request_queue()
func _on_request_completed(
result: int,
response_code: int,
_headers: PackedStringArray,
body: PackedByteArray,
http_client: HTTPRequest
) -> void:
var request = http_client.get_meta("request_data", {})
if request.is_empty():
return
# Mark client as available
http_client.set_meta("busy", false)
http_client.remove_meta("request_data")
_active_requests -= 1
if result != HTTPRequest.RESULT_SUCCESS:
_handle_request_error(request, "Request failed: " + str(result))
return
var response_text = body.get_string_from_utf8()
var response_data = {}
if response_text:
var json = JSON.new()
if json.parse(response_text) == OK:
response_data = json.data
else:
print("[GumYum Game] Failed to parse JSON response: ", response_text)
if debug_mode:
print("[GumYum Game] Response code: ", response_code)
if response_code < 200 or response_code >= 300:
print("[GumYum Game] Response body: ", response_text.substr(0, 200)) # First 200 chars
if response_code >= 200 and response_code < 300:
_handle_success_response(request, response_data)
else:
_handle_error_response(request, response_code, response_data)
# Process next request
_process_request_queue()
func _handle_success_response(request: Dictionary, data: Dictionary) -> void:
request_completed.emit(request.endpoint, data)
if request.callback.is_valid():
request.callback.call(data)
func _handle_error_response(request: Dictionary, code: int, data: Dictionary) -> void:
var error_msg = data.get("message", data.get("error", "Unknown error"))
if debug_mode:
print(
(
"[GumYum Game] Error response for %s: HTTP %d - %s"
% [request.endpoint, code, error_msg]
)
)
if data.has("details"):
print("[GumYum Game] Details: ", data.get("details"))
# Handle 403 - try to refresh token only for expired tokens
if code == 403 and _refresh_token != "" and request.retries == 0:
# Check if it's specifically an expired token error
var error_message = data.get("error", "").to_lower()
if error_message.contains("expired") and error_message.contains("token"):
if debug_mode:
print("[GumYum Game] Got 403 with expired token, attempting token refresh...")
request.retries += 1
_pending_requests.push_front(request)
_refresh_jwt_token()
return
# For 401 errors (like revoked API keys), don't retry - just fail
if code == 401:
if debug_mode:
print("[GumYum Game] Got 401 Unauthorized - not retrying")
_handle_request_error(request, "Error %d: %s" % [code, error_msg])
return
# Retry logic for temporary failures
if code >= 500 and request.retries < max_retries:
request.retries += 1
if debug_mode:
print("[GumYum Game] Retrying request (attempt %d/%d)" % [request.retries, max_retries])
# Exponential backoff
await get_tree().create_timer(pow(2, request.retries)).timeout
_pending_requests.push_front(request)
return
_handle_request_error(request, "Error %d: %s" % [code, error_msg])
func _handle_request_error(request: Dictionary, error: String) -> void:
if debug_mode:
print("[GumYum Game] Request failed: %s - %s" % [request.endpoint, error])
request_failed.emit(request.endpoint, error)
if request.callback.is_valid():
request.callback.call({"error": error})
func _exchange_api_key_for_jwt() -> void:
if _api_key == "" or _api_secret == "":
return
if _auth_in_progress:
return # Already exchanging, skip
_auth_in_progress = true
var data = {"public_key": _api_key, "secret_key": _api_secret}
var url = get_api_url("auth/exchange")
var headers = get_headers()
var body = JSON.stringify(data)
# Use first HTTP client for auth
var auth_client = _http_clients[0]
var error = auth_client.request_raw(url, headers, HTTPClient.METHOD_POST, body.to_utf8_buffer())
if error == OK:
# Wait for response
var result = await auth_client.request_completed
if result[1] == 200: # response_code
var response_text = result[3].get_string_from_utf8() # body
var json = JSON.new()
if json.parse(response_text) == OK:
var response_data = json.data
_jwt_token = response_data.get("access_token", "")
_refresh_token = response_data.get("refresh_token", "")
var expires_in = response_data.get("expires_in", 3600)
_token_expires_at = Time.get_ticks_msec() / 1000.0 + expires_in
if debug_mode:
print("[GumYum Game] API key exchanged for JWT")
print("[GumYum Game] Got refresh token: ", _refresh_token != "")
_auth_in_progress = false
# Process queued requests
_process_request_queue()
else:
# Handle authentication error
var error_text = result[3].get_string_from_utf8() if result[3] else "Unknown error"
push_error(
"[GumYum Game] API key exchange failed (code %d): %s" % [result[1], error_text]
)
# Try to parse error message
var json = JSON.new()
if json.parse(error_text) == OK and json.data is Dictionary:
var error_msg = json.data.get("error", json.data.get("message", "Invalid API keys"))
_auth_in_progress = false
# Process pending requests with error
while _pending_requests.size() > 0:
var req = _pending_requests.pop_front()
_handle_request_error(req, "Authentication failed: " + error_msg)
else:
_auth_in_progress = false
# Process pending requests with generic error
while _pending_requests.size() > 0:
var req = _pending_requests.pop_front()
_handle_request_error(req, "Invalid public key or secret key")
else:
_auth_in_progress = false
push_error("[GumYum Game] Failed to make exchange request: " + error_string(error))
func _refresh_jwt_token() -> void:
if _refresh_token == "":
return
if _auth_in_progress:
return # Already refreshing, skip
_auth_in_progress = true
var data = {"refresh_token": _refresh_token}
var url = get_api_url("auth/refresh")
var headers = get_headers()
var body = JSON.stringify(data)
if debug_mode:
print("[GumYum Game] Refreshing JWT token...")
# Use first HTTP client for auth
var auth_client = _http_clients[0]
var error = auth_client.request_raw(url, headers, HTTPClient.METHOD_POST, body.to_utf8_buffer())
if error == OK:
# Wait for response
var result = await auth_client.request_completed
if result[1] == 200: # response_code
var response_text = result[3].get_string_from_utf8() # body
var json = JSON.new()
if json.parse(response_text) == OK:
var response_data = json.data
_jwt_token = response_data.get("access_token", "")
# Server may rotate refresh token
if response_data.has("refresh_token"):
_refresh_token = response_data.get("refresh_token", "")
var expires_in = response_data.get("expires_in", 3600)
_token_expires_at = Time.get_ticks_msec() / 1000.0 + expires_in
if debug_mode:
print("[GumYum Game] JWT token refreshed successfully")
_auth_in_progress = false
# Process queued requests
_process_request_queue()
elif result[1] == 403: # Forbidden - refresh token invalid
# Handle refresh token expiry
var error_text = result[3].get_string_from_utf8() if result[3] else "Unknown error"
push_error(
"[GumYum Game] Refresh token expired or invalid (code 403): %s" % [error_text]
)
# Clear tokens
_jwt_token = ""
_refresh_token = ""
_auth_in_progress = false
# If we have API keys, try to re-exchange
if _api_key != "":
if debug_mode:
print("[GumYum Game] Refresh failed, re-exchanging API key...")
_exchange_api_key_for_jwt()
else:
# Process pending requests with error
while _pending_requests.size() > 0:
var req = _pending_requests.pop_front()
_handle_request_error(req, "Authentication expired - please re-authenticate")
else:
# Handle other refresh errors
var error_text = result[3].get_string_from_utf8() if result[3] else "Unknown error"
push_error("[GumYum Game] Token refresh failed (code %d): %s" % [result[1], error_text])
_auth_in_progress = false
# Process pending requests with error
while _pending_requests.size() > 0:
var req = _pending_requests.pop_front()
_handle_request_error(req, "Token refresh failed")
else:
_auth_in_progress = false
push_error("[GumYum Game] Failed to make refresh request: " + error_string(error))
## Internal method for streaming requests
func _request_stream(
method: HTTPClient.Method,
endpoint: String,
data: Dictionary = {},
query_params: Dictionary = {},
callback: Callable = Callable()
) -> void:
# Check if we need to wait for auth
if not is_ready():
push_error("[GumYum Stream] Not authenticated yet")
if callback.is_valid():
callback.call({"error": "Not authenticated"})
return
# Create a new thread for streaming
var thread = Thread.new()
thread.start(_stream_request_thread.bind(method, endpoint, data, query_params, callback))
func _stream_request_thread(
method: HTTPClient.Method,
endpoint: String,
data: Dictionary,
query_params: Dictionary,
callback: Callable
) -> void:
var http = HTTPClient.new()
var url = get_api_url(endpoint)
var parsed_url = url.split("/")
var host = parsed_url[2].split(":")[0]
var port = 443 if url.begins_with("https") else 80
if ":" in parsed_url[2]:
port = int(parsed_url[2].split(":")[1])
var path = "/" + "/".join(parsed_url.slice(3))
if debug_mode:
print("[GumYum Stream] Connecting to %s:%d%s" % [host, port, path])
# Add query parameters
if query_params.size() > 0:
var query_parts = []
for key in query_params:
query_parts.append(key + "=" + str(query_params[key]).uri_encode())
path += "?" + "&".join(query_parts)
# Connect to server
var err = OK
if port == 443:
# For HTTPS, create TLS options
var tls_options = TLSOptions.client()
err = http.connect_to_host(host, port, tls_options)
else:
# For HTTP, no TLS needed
err = http.connect_to_host(host, port)
if err != OK:
call_deferred("_handle_stream_error", endpoint, "Failed to connect: " + str(err), callback)
return
# Wait for connection
while (
http.get_status() == HTTPClient.STATUS_CONNECTING
or http.get_status() == HTTPClient.STATUS_RESOLVING
):
http.poll()
OS.delay_msec(10)
if http.get_status() != HTTPClient.STATUS_CONNECTED:
call_deferred("_handle_stream_error", endpoint, "Connection failed", callback)
return
# Prepare headers
var headers = get_headers()
headers.append("Accept: text/event-stream")
# Make request
var body_string = JSON.stringify(data)
err = http.request(method, path, headers, body_string)
if err != OK:
call_deferred("_handle_stream_error", endpoint, "Request failed: " + str(err), callback)
return
# Process streaming response
call_deferred("emit_signal", "dialogue_stream_started")
var response_data = ""
var accumulated_content = ""
var context = {}
var mood_transition = {}
var chunk_count = 0
while http.get_status() == HTTPClient.STATUS_REQUESTING:
http.poll()
OS.delay_msec(1)
if (
http.get_status() != HTTPClient.STATUS_BODY
and http.get_status() != HTTPClient.STATUS_CONNECTED
):
call_deferred(
"_handle_stream_error",
endpoint,
"Request failed with status: " + str(http.get_status()),
callback
)
return
if debug_mode:
print("[GumYum Stream] Starting to read response body...")
# Read streaming response
while http.get_status() == HTTPClient.STATUS_BODY:
http.poll()
var chunk = http.read_response_body_chunk()
if chunk.size() > 0:
response_data += chunk.get_string_from_utf8()
# Process SSE events
var lines = response_data.split("\n")
for i in range(lines.size() - 1):
var line = lines[i].strip_edges()
if line.begins_with("data: "):
var data_str = line.substr(6)
if debug_mode:
print("[GumYum Stream] Received data: " + data_str.substr(0, 100))
if data_str == "[DONE]" or data_str == "done":
# Stream finished
if debug_mode:
print(
(
"[GumYum Stream] Stream complete, total content: "
+ str(accumulated_content.length())
+ " chars"
)
)
call_deferred(
"emit_signal",
"dialogue_stream_ended",
accumulated_content,
context,
mood_transition
)
if callback.is_valid():
call_deferred(
"_call_callback",
callback,
{
"choices": [{"message": {"content": accumulated_content}}],
"npc_context": context,
"mood_transition": mood_transition
}
)
return
# Parse JSON data
var json = JSON.new()
if json.parse(data_str) == OK:
var event_data = json.data
# Handle different event types
if event_data.has("choices") and event_data.choices.size() > 0:
var choice = event_data.choices[0]
if choice.has("delta") and choice.delta.has("content"):
var content_chunk = choice.delta.content
accumulated_content += content_chunk
chunk_count += 1
if debug_mode:
print(
(
"[GumYum Stream] Chunk %d: %s"
% [chunk_count, content_chunk]
)
)
call_deferred(
"emit_signal", "dialogue_chunk_received", content_chunk
)
elif choice.has("message") and choice.message.has("content"):
# Full message format
var content = choice.message.content
accumulated_content = content
if debug_mode:
print(
(
"[GumYum Stream] Full message received: "
+ content.substr(0, 50)
+ "..."
)
)
call_deferred("emit_signal", "dialogue_chunk_received", content)
# Check for context and mood transition
if event_data.has("npc_context"):
context = event_data.npc_context
if event_data.has("mood_transition"):
mood_transition = event_data.mood_transition
# Keep the last incomplete line for next iteration
response_data = lines[lines.size() - 1]
OS.delay_msec(1)
# Ensure we emit the final response
call_deferred(
"emit_signal", "dialogue_stream_ended", accumulated_content, context, mood_transition
)
if callback.is_valid():
call_deferred(
"_call_callback",
callback,
{
"choices": [{"message": {"content": accumulated_content}}],
"npc_context": context,
"mood_transition": mood_transition
}
)
func _handle_stream_error(endpoint: String, error: String, callback: Callable) -> void:
push_error("[GumYum Game] Stream error on %s: %s" % [endpoint, error])
request_failed.emit(endpoint, error)
if callback.is_valid():
callback.call({"error": error})
func _call_callback(callback: Callable, data: Dictionary) -> void:
if callback.is_valid():
callback.call(data)