feat(city): add airports, ATC agent, luminous signs, and AI model fallback chain
- Add two airports (north/south) with full geometry: runways, taxiways,
aprons, terminals, hangars, stands, and helipads; walkable airport
grounds extend beyond city bounds.
- Introduce ATC AI agent ("Torre de Controle City10") using AI_ATC_MODEL
to generate takeoff clearances; includes dispatch endpoint with
30-second cooldown and deterministic fallback on failure.
- Implement AI model fallback chain: preferred model → AI_FALLBACK_MODEL
before failing, applied in ChatController and PersonTalkController.
- Add luminous sign labels (letreiros) for plazas, beach, food, hospital,
school, city hall, and airports with day/night glow.
- Fix city presence cache: compute is_me per viewer instead of baking
into shared per-second cache to prevent cross-user flag leakage.
- Add tests: AirportTest, CityPresenceTest, SignLabelsTest, and
PersonTalkTest fallback model retry scenario.
This commit is contained in:
@@ -127,22 +127,51 @@ class ChatController extends Controller
|
||||
."\n\nContexto simulado da cidade: ".json_encode($cityContext, JSON_UNESCAPED_UNICODE)
|
||||
."\nResponda de forma curta e natural, em português. Não invente dados pessoais de usuários.";
|
||||
|
||||
$response = Http::withHeaders([
|
||||
'Authorization' => 'Bearer '.$apiKey,
|
||||
'HTTP-Referer' => config('ai.openrouter.app_url'),
|
||||
'X-Title' => config('ai.openrouter.app_name'),
|
||||
])->timeout((int) config('ai.openrouter.timeout', 30))
|
||||
->post(config('ai.openrouter.base_url').'/chat/completions', [
|
||||
'model' => $agent->model,
|
||||
'messages' => [
|
||||
['role' => 'system', 'content' => $system],
|
||||
['role' => 'user', 'content' => $userMessage],
|
||||
],
|
||||
'max_tokens' => 300,
|
||||
]);
|
||||
// Preferred agent model first, then the configured fallback
|
||||
// chain (AI_FALLBACK_MODEL) before failing the turn.
|
||||
$models = [$agent->model];
|
||||
$fallback = config('ai.fallback_model');
|
||||
if (is_string($fallback) && $fallback !== ''
|
||||
&& $fallback !== $agent->model
|
||||
&& in_array($fallback, config('ai.allowed_models', []), true)) {
|
||||
$models[] = $fallback;
|
||||
}
|
||||
|
||||
if ($response->failed()) {
|
||||
self::fail($agent, 'HTTP '.$response->status());
|
||||
$response = null;
|
||||
foreach ($models as $candidate) {
|
||||
try {
|
||||
$response = Http::withHeaders([
|
||||
'Authorization' => 'Bearer '.$apiKey,
|
||||
'HTTP-Referer' => config('ai.openrouter.app_url'),
|
||||
'X-Title' => config('ai.openrouter.app_name'),
|
||||
])->timeout((int) config('ai.openrouter.timeout', 30))
|
||||
->post(config('ai.openrouter.base_url').'/chat/completions', [
|
||||
'model' => $candidate,
|
||||
'messages' => [
|
||||
['role' => 'system', 'content' => $system],
|
||||
['role' => 'user', 'content' => $userMessage],
|
||||
],
|
||||
'max_tokens' => 300,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('NPC think model exception', ['model' => $candidate, 'error' => $e->getMessage()]);
|
||||
$response = null;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($response->failed()) {
|
||||
Log::warning('NPC think model failed', ['model' => $candidate, 'status' => $response->status()]);
|
||||
$response = null;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if ($response === null) {
|
||||
self::fail($agent, 'all models failed');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user