extends Node const OLLAMA_URL: String = "http://127.0.0.1:11434/api/generate" const DEFAULT_MODEL: String = "mistral-nemo" const REQUEST_TIMEOUT: float = 25.0 var _http: HTTPRequest var _pending: Dictionary = {} var _pending_target: Node = null func _ready() -> void: _http = HTTPRequest.new() _http.timeout = REQUEST_TIMEOUT add_child(_http) _http.request_completed.connect(_on_response) func ask(npc: Node, player: Node, question: String) -> void: if not (multiplayer.is_server() or multiplayer.multiplayer_peer == null): request_ask.rpc_id(1, npc.get_path(), player.get_path(), question) return var profile: NpcProfile = npc.profile var world_context: String = GameLore.build_npc_context(profile) var system_prompt: String = "%s\n\nDU BIST:\nName: %s\nHintergrund: %s\nPersönlichkeit: %s\n\nREGELN:\n- Antworte kurz: maximal 2 Sätze.\n- Antworte auf Deutsch.\n- Bleibe in deiner Rolle, schreibe NIE 'Als KI...' oder Ähnliches.\n- Nutze die WELT-Begriffe (Schlund, Atemzug, Reisender, Anker, Anderseite).\n- Wenn dich etwas gefragt wird, das du nicht wissen kannst, sag das ehrlich.\n- Erfinde keine Fakten, die nicht in der WELT stehen." % [world_context, profile.display_name, profile.lore, profile.personality] var prompt: String = "Reisender: %s\n%s:" % [question, profile.display_name] _send_request(npc, player, system_prompt, prompt) @rpc("any_peer", "reliable") func request_ask(npc_path: NodePath, player_path: NodePath, question: String) -> void: var npc: Node = get_node_or_null(npc_path) var player: Node = get_node_or_null(player_path) if npc and player: ask(npc, player, question) func _send_request(npc: Node, player: Node, system_prompt: String, prompt: String) -> void: var body: Dictionary = { "model": DEFAULT_MODEL, "prompt": prompt, "system": system_prompt, "stream": false, "options": {"temperature": 0.7, "num_predict": 120} } var headers := PackedStringArray(["Content-Type: application/json"]) var json := JSON.stringify(body) var token: int = randi() _pending[token] = {"npc": npc, "player": player} _pending_target = player var err := _http.request(OLLAMA_URL, headers, HTTPClient.METHOD_POST, json) if err != OK: _send_fallback(npc, player) func _on_response(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void: var entry_keys: Array = _pending.keys() if entry_keys.is_empty(): return var token: int = entry_keys[0] var entry: Dictionary = _pending[token] _pending.erase(token) var npc: Node = entry.npc var player: Node = entry.player if result != HTTPRequest.RESULT_SUCCESS or response_code != 200: _send_fallback(npc, player) return var text: String = body.get_string_from_utf8() var data: Variant = JSON.parse_string(text) if typeof(data) != TYPE_DICTIONARY or not "response" in data: _send_fallback(npc, player) return var answer: String = (data.response as String).strip_edges() if answer == "": _send_fallback(npc, player) return _deliver(player, npc, answer) func _send_fallback(npc: Node, player: Node) -> void: if not is_instance_valid(npc): return var profile: NpcProfile = npc.profile _deliver(player, npc, profile.fallback_text) func _deliver(player: Node, npc: Node, text: String) -> void: if player == null or not is_instance_valid(player): return var auth: int = player.get_multiplayer_authority() _on_dialog_answer.rpc_id(auth, npc.get_path(), text) @rpc("authority", "reliable", "call_local") func _on_dialog_answer(npc_path: NodePath, text: String) -> void: var npc: Node = get_node_or_null(npc_path) if npc: EventBus.chat_message.emit(0, npc.profile.display_name if npc.has_method("get") else "NPC", text) for hud in get_tree().get_nodes_in_group("dialog_ui"): if hud.has_method("show_answer"): hud.show_answer(text)