Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
## Project Overview
FastAPI-based stock trading/investing platform focused on Brazilian stocks (B3). Multi-service architecture: USER, STOCKS_API, PROMETHEUS (AI chat), SCRAPER.
FastAPI-based stock trading/investing platform focused on Brazilian stocks (B3). Multi-service architecture: USER, STOCKS_API, ORUNMILA (AI chat), SCRAPER.

## Dev Commands
```bash
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,11 @@ STOCKSAPI_DEFAULT.QUOTA=5000
STOCKSAPI_QUOTA.RESETDAYS=30

#
#$ PROMETHEUS
#$ ORUNMILA
#
PROMETHEUS_ENABLED=TRUE
PROMETHEUS_HOST=localhost
PROMETHEUS_PORT=3200
ORUNMILA_ENABLED=TRUE
ORUNMILA_HOST=localhost
ORUNMILA_PORT=3200
GEMINI_API.KEY=key
SEARXNG_URL=http://searxng:8888
FORGEVM_URL=http://forgevm:7423
Expand Down
2 changes: 1 addition & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

- [ ] Abacate Pay
- [ ] nginx sacling for autoscaling workers with tailored configs triggering the required services only (support for remote vps's)
- [ ] forgevm scaling via multiple hosts, simple router in business code and hostId tracking in the prometheus_sandbox model to select a host for the sandbox, rclone for /workspace cloning
- [ ] forgevm scaling via multiple hosts, simple router in business code and hostId tracking in the orunmila_sandbox model to select a host for the sandbox, rclone for /workspace cloning
- [ ] drop {service_name}_PORT and {serivce_name}_HOST and replace it with nginx managed services
- [ ] p99 latency and context switches monitor at /status
- [ ] feather cache for synced cache and with redis for a flock replacement
Expand Down
92 changes: 46 additions & 46 deletions config.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import os
import socket
from typing import Optional
from pydantic import Field, AliasChoices
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
from sqlalchemy import create_engine, QueuePool
from sqlalchemy.orm import sessionmaker
Expand All @@ -21,78 +21,78 @@ def new_getaddrinfo(*args, **kwargs):


class MysqlSettings(BaseSettings):
USER_USER: Optional[str] = Field(default=None, validation_alias=AliasChoices("USER_MYSQL_USER"))
USER_PASSWORD: Optional[str] = Field(default=None, validation_alias=AliasChoices("USER_MYSQL_PASSWORD"))
USER_HOST: Optional[str] = Field(default=None, validation_alias=AliasChoices("USER_MYSQL_HOST"))
USER_DATABASE: Optional[str] = Field(default=None, validation_alias=AliasChoices("USER_MYSQL_DATABASE"))
USER_PORT: int = Field(default=3306, validation_alias=AliasChoices("USER_MYSQL_PORT"))

STOCKS_USER: Optional[str] = Field(default=None, validation_alias=AliasChoices("STOCKS_MYSQL_USER"))
STOCKS_PASSWORD: Optional[str] = Field(default=None, validation_alias=AliasChoices("STOCKS_MYSQL_PASSWORD"))
STOCKS_HOST: Optional[str] = Field(default=None, validation_alias=AliasChoices("STOCKS_MYSQL_HOST"))
STOCKS_DATABASE: Optional[str] = Field(default=None, validation_alias=AliasChoices("STOCKS_MYSQL_DATABASE"))
STOCKS_PORT: int = Field(default=3306, validation_alias=AliasChoices("STOCKS_MYSQL_PORT"))
USER_USER: Optional[str] = Field(default=None, validation_alias="USER_MYSQL_USER")
USER_PASSWORD: Optional[str] = Field(default=None, validation_alias="USER_MYSQL_PASSWORD")
USER_HOST: Optional[str] = Field(default=None, validation_alias="USER_MYSQL_HOST")
USER_DATABASE: Optional[str] = Field(default=None, validation_alias="USER_MYSQL_DATABASE")
USER_PORT: int = Field(default=3306, validation_alias="USER_MYSQL_PORT")

STOCKS_USER: Optional[str] = Field(default=None, validation_alias="STOCKS_MYSQL_USER")
STOCKS_PASSWORD: Optional[str] = Field(default=None, validation_alias="STOCKS_MYSQL_PASSWORD")
STOCKS_HOST: Optional[str] = Field(default=None, validation_alias="STOCKS_MYSQL_HOST")
STOCKS_DATABASE: Optional[str] = Field(default=None, validation_alias="STOCKS_MYSQL_DATABASE")
STOCKS_PORT: int = Field(default=3306, validation_alias="STOCKS_MYSQL_PORT")
model_config = SettingsConfigDict(env_file=".env", extra="ignore")


class UserSettings(BaseSettings):
ENABLED: bool = Field(default=True, validation_alias=AliasChoices("USER_ENABLED"))
HOST: str = Field(default="localhost", validation_alias=AliasChoices("USER_HOST"))
PORT: int = Field(default=3200, validation_alias=AliasChoices("USER_PORT"))
JWT_SECRET_KEY: str = Field(default=..., validation_alias=AliasChoices("JWT_SECRET_KEY"))
SESSION_SECRET_KEY: str = Field(default=..., validation_alias=AliasChoices("SESSION_SECRET_KEY"))
GOOGLE_CLIENT_ID: str = Field(default="", validation_alias=AliasChoices("GOOGLE_CLIENT.ID"))
GOOGLE_CLIENT_SECRET: str = Field(default="", validation_alias=AliasChoices("GOOGLE_CLIENT.SECRET"))
GOOGLE_REDIRECT_URI: str = Field(default="", validation_alias=AliasChoices("GOOGLE_REDIRECT.URI"))
ENABLED: bool = Field(default=True, validation_alias="USER_ENABLED")
HOST: str = Field(default="localhost", validation_alias="USER_HOST")
PORT: int = Field(default=3200, validation_alias="USER_PORT")
JWT_SECRET_KEY: str = Field(default=..., validation_alias="JWT_SECRET_KEY")
SESSION_SECRET_KEY: str = Field(default=..., validation_alias="SESSION_SECRET_KEY")
GOOGLE_CLIENT_ID: str = Field(default="", validation_alias="GOOGLE_CLIENT.ID")
GOOGLE_CLIENT_SECRET: str = Field(default="", validation_alias="GOOGLE_CLIENT.SECRET")
GOOGLE_REDIRECT_URI: str = Field(default="", validation_alias="GOOGLE_REDIRECT.URI")
model_config = SettingsConfigDict(env_file=".env", extra="ignore")


class StocksApiSettings(BaseSettings):
ENABLED: bool = Field(default=True, validation_alias=AliasChoices("STOCKSAPI_ENABLED"))
HOST: str = Field(default="localhost", validation_alias=AliasChoices("STOCKSAPI_HOST"))
PORT: int = Field(default=3200, validation_alias=AliasChoices("STOCKSAPI_PORT"))
KEY_SYSTEM: bool = Field(default=False, validation_alias=AliasChoices("STOCKSAPI_KEY.SYSTEM"))
KEY: str = Field(default="", validation_alias=AliasChoices("STOCKSAPI_PRIVATE.KEY"))
ENABLED: bool = Field(default=True, validation_alias="STOCKSAPI_ENABLED")
HOST: str = Field(default="localhost", validation_alias="STOCKSAPI_HOST")
PORT: int = Field(default=3200, validation_alias="STOCKSAPI_PORT")
KEY_SYSTEM: bool = Field(default=False, validation_alias="STOCKSAPI_KEY.SYSTEM")
KEY: str = Field(default="", validation_alias="STOCKSAPI_PRIVATE.KEY")
model_config = SettingsConfigDict(env_file=".env", extra="ignore")


class PrometheusSettings(BaseSettings):
ENABLED: bool = Field(default=True, validation_alias=AliasChoices("PROMETHEUS_ENABLED"))
HOST: str = Field(default="localhost", validation_alias=AliasChoices("PROMETHEUS_HOST"))
PORT: int = Field(default=3200, validation_alias=AliasChoices("PROMETHEUS_PORT"))
GEMINI_API_KEY: str = Field(default="", validation_alias=AliasChoices("GEMINI_API.KEY"))
SEARXNG_URL: str = Field(default="http://searxng:8888", validation_alias=AliasChoices("SEARXNG_URL"))
FORGEVM_URL: str = Field(default="http://forgevm:7423", validation_alias=AliasChoices("FORGEVM_URL"))
FORGEVM_API_TOKEN: str = Field(default="", validation_alias=AliasChoices("FORGEVM_API_TOKEN"))
SANDBOX_IMAGE: str = Field(default="sandbox-python:latest", validation_alias=AliasChoices("SANDBOX_IMAGE"))
SANDBOX_MEMORY: int = Field(default=512, validation_alias=AliasChoices("SANDBOX_MEMORY"))
SANDBOX_CPU: int = Field(default=1, validation_alias=AliasChoices("SANDBOX_CPU"))
SANDBOX_TTL: int = Field(default=5, validation_alias=AliasChoices("SANDBOX_TTL"))
WORKSPACE_MAX_UPLOAD_MB: int = Field(default=10, validation_alias=AliasChoices("WORKSPACE_MAX_UPLOAD_MB"))
class OrunmilaSettings(BaseSettings):
ENABLED: bool = Field(default=True, validation_alias="ORUNMILA_ENABLED")
HOST: str = Field(default="localhost", validation_alias="ORUNMILA_HOST")
PORT: int = Field(default=3200, validation_alias="ORUNMILA_PORT")
GEMINI_API_KEY: str = Field(default="", validation_alias="GEMINI_API.KEY")
SEARXNG_URL: str = Field(default="http://searxng:8888", validation_alias="SEARXNG_URL")
FORGEVM_URL: str = Field(default="http://forgevm:7423", validation_alias="FORGEVM_URL")
FORGEVM_API_TOKEN: str = Field(default="", validation_alias="FORGEVM_API_TOKEN")
SANDBOX_IMAGE: str = Field(default="sandbox-python:latest", validation_alias="SANDBOX_IMAGE")
SANDBOX_MEMORY: int = Field(default=512, validation_alias="SANDBOX_MEMORY")
SANDBOX_CPU: int = Field(default=1, validation_alias="SANDBOX_CPU")
SANDBOX_TTL: int = Field(default=5, validation_alias="SANDBOX_TTL")
WORKSPACE_MAX_UPLOAD_MB: int = Field(default=10, validation_alias="WORKSPACE_MAX_UPLOAD_MB")

model_config = SettingsConfigDict(env_file=".env", extra="ignore")


class ScraperSettings(BaseSettings):
ENABLED: bool = Field(default=False, validation_alias=AliasChoices("SCRAPER_ENABLED"))
SCHEDULER: str = Field(default="", validation_alias=AliasChoices("SCRAPER_SCHEDULER"))
JSON: bool = Field(default=False, validation_alias=AliasChoices("JSON_EXPORT"))
MYSQL: bool = Field(default=True, validation_alias=AliasChoices("MYSQL_EXPORT"))
MAX_WORKERS: int = Field(default=10, validation_alias=AliasChoices("MAX_WORKERS"))
ENABLED: bool = Field(default=False, validation_alias="SCRAPER_ENABLED")
SCHEDULER: str = Field(default="", validation_alias="SCRAPER_SCHEDULER")
JSON: bool = Field(default=False, validation_alias="JSON_EXPORT")
MYSQL: bool = Field(default=True, validation_alias="MYSQL_EXPORT")
MAX_WORKERS: int = Field(default=10, validation_alias="MAX_WORKERS")
model_config = SettingsConfigDict(env_file=".env", extra="ignore")


class DiscordSettings(BaseSettings):
ENABLED: bool = Field(default=False, validation_alias=AliasChoices("DISCORD_ENABLED"))
WEBHOOK_URL: str = Field(default="", validation_alias=AliasChoices("DISCORD_WEBHOOK_URL"))
ENABLED: bool = Field(default=False, validation_alias="DISCORD_ENABLED")
WEBHOOK_URL: str = Field(default="", validation_alias="DISCORD_WEBHOOK_URL")
model_config = SettingsConfigDict(env_file=".env", extra="ignore")


class Config:
DEBUG_MODE: bool = os.getenv("DEBUG_MODE", "FALSE").upper() == "TRUE"
MYSQL = MysqlSettings()
STOCKS_API = StocksApiSettings()
PROMETHEUS = PrometheusSettings()
ORUNMILA = OrunmilaSettings()
SCRAPER = ScraperSettings()
USER = UserSettings()
DISCORD = DiscordSettings()
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ services:
ports:
- "8000:8000"
- "${STOCKSAPI_PORT:-3200}:${STOCKSAPI_PORT:-3200}"
- "${PROMETHEUS_PORT:-3200}:${PROMETHEUS_PORT:-3200}"
- "${ORUNMILA_PORT:-3200}:${ORUNMILA_PORT:-3200}"
- "${USER_PORT:-3200}:${USER_PORT:-3200}"
environment:
- DEBUG_MODE=TRUE
Expand Down
4 changes: 2 additions & 2 deletions docs/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,12 @@ Parsing: `parseDeviceFields` (`main/app/authentication/session.py:13-27`, stored
| Role | Effective permissions |
| :--- | :--- |
| `USER` | none (`Permission.NONE`) |
| `PREMIUM` | `USE_PROMETHEUS` + `PROMETHEUS_EXTENDED_MEMORIES` |
| `PREMIUM` | `USE_ORUNMILA` + `ORUNMILA_EXTENDED_MEMORIES` |
| `DEVELOPER_STARTER` | = `USER` (no extra permissions) |
| `DEVELOPER_ENTERPRISE` | = `DEVELOPER_STARTER` (no extra permissions) |
| `ADMIN` | all (`Permission.ALL()`), bypasses checks |

Only two permissions exist: `USE_PROMETHEUS`, `PROMETHEUS_EXTENDED_MEMORIES`. There are no `VIEW_PROFILE` / `USE_THOTH` / `USE_MAAT` / `USE_OGUM` permissions — delete any such claims.
Only two permissions exist: `USE_ORUNMILA`, `ORUNMILA_EXTENDED_MEMORIES`. There are no `VIEW_PROFILE` / `USE_THOTH` / `USE_MAAT` / `USE_OGUM` permissions — delete any such claims.

## Rate limits

Expand Down
56 changes: 27 additions & 29 deletions docs/orunmila.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
# Orunmila

O Orunmila é um agente chatbot focado no domínio financeiro, com acesso à carteira do usuário (sistema de carteira Iyagba/Thoth, em desenvolvimento), a dados financeiros (Stocks API) e a um sandbox para análises de computação estatística (ForgeVM), com um poderoso sistema de memória. Ele é a proposta de renomeação do Prometheus.

> Nota: os identificadores de código (env `PROMETHEUS_*`, rotas `/prometheus/*`, tabelas `prometheus`) permanecem inalterados até a renomeação posterior do código.
O Orunmila é um agente chatbot focado no domínio financeiro, com acesso à carteira do usuário (sistema de carteira Iyagba/Thoth, em desenvolvimento), a dados financeiros (Stocks API) e a um sandbox para análises de computação estatística (ForgeVM), com um poderoso sistema de memória. Ele foi renomeado de Prometheus.

## Usage

1. Environment configuration (`.env`) — `PrometheusSettings` (`config.py:59-71`):
1. Environment configuration (`.env`) — `OrunmilaSettings` (`config.py:59-71`):

```env
PROMETHEUS_ENABLED=TRUE
PROMETHEUS_HOST=localhost
PROMETHEUS_PORT=3200
ORUNMILA_ENABLED=TRUE
ORUNMILA_HOST=localhost
ORUNMILA_PORT=3200
GEMINI_API.KEY=your_api_key_here
SEARXNG_URL=http://searxng:8888
FORGEVM_URL=http://forgevm:7423
Expand All @@ -25,7 +23,7 @@ O Orunmila é um agente chatbot focado no domínio financeiro, com acesso à car

2. Database Schema:

`prometheus` (`main/models/prometheus.py:7-16`):
`orunmila` (`main/models/orunmila.py:7-16`):

* `sessionId`: String(255) (PK)
* `userId`: Integer (FK → users.userId, ondelete CASCADE)
Expand All @@ -35,7 +33,7 @@ O Orunmila é um agente chatbot focado no domínio financeiro, com acesso à car
* `lastActivity`: TIMESTAMP (server_default now, onupdate now)
* `createdAt`: TIMESTAMP (server_default now)

`prometheus_memories` (`main/models/memory.py:8-34`):
`orunmila_memories` (`main/models/memory.py:8-34`):

* `id`: Integer (PK, autoincrement)
* `userId`: Integer (indexed)
Expand All @@ -49,7 +47,7 @@ O Orunmila é um agente chatbot focado no domínio financeiro, com acesso à car
* `contentHash`: String(32)
* `createdAt` / `updatedAt`: DateTime (server_default now; `updatedAt` onupdate now)
* `lastAccessedAt` / `archivedAt`: DateTime (nullable)
* Constraints: `UniqueConstraint(userId, memoryKey)` (`uk_prometheus_memories`), indexes `idx_relevance(userId, score)` and `idx_type(userId, memoryType)`
* Constraints: `UniqueConstraint(userId, memoryKey)` (`uk_orunmila_memories`), indexes `idx_relevance(userId, score)` and `idx_type(userId, memoryType)`

3. Run the server:

Expand All @@ -59,32 +57,32 @@ O Orunmila é um agente chatbot focado no domínio financeiro, com acesso à car

## Workflow

30-turn Gemini-native tool-calling loop (`main/app/prometheus/agent.py:52,316,370`):
30-turn Gemini-native tool-calling loop (`main/app/orunmila/agent.py:52,316,370`):

* `MAX_TURNS = 30` (`agent.py:52`); main loop `while turn < MAX_TURNS` (`agent.py:316`).
* Each turn: stream Gemini chunks → collect `function_calls` → `dispatchToolCall` (MCP sessions + local `TOOL_REGISTRY`) → append tool results → re-send conversation until a text-only turn or `turn_limit`.
* Hitting the cap yields `{"type": "turn_limit", "maxTurns": 30}`.
* Model: `gemini-flash-lite-latest`; chat temperature `0.5` (`agent.py:245,250`), memory extraction temperature `0.2` (`main/app/prometheus/memory.py:493,497`).
* `TOOL_REGISTRY` — 7 tools (`main/app/prometheus/tools.py:179-187`): `search_memory`, `save_memory`, `execute_code`, `read_file`, `write_file`, `list_files`, `serve_file`.
* ForgeVM sandbox per chat turn (`main/app/prometheus/sandbox.py:24-66`): `spawn(image, memory_mb, vcpus, ttl)` from `PROMETHEUS_*` settings; per-user workspace under `/workspace/{userId}` with path-traversal guard (`hostPath`).
* Streaming over SSE via `sse_starlette` (`main/app/prometheus/stream_bus.py:6,109`): `JSONServerSentEvent` generator + `EventSourceResponse(..., ping=15)`.
* Memory (`main/app/prometheus/memory.py`): fused rank `0.6 * vector + 0.25 * fulltext + 0.15 * recency` (`:173`); caps 50 basic / 250 premium (`:72-73`); `rapidfuzz` dedup (`:134`); `cashews` cache matrix (`:30`); deferred BLOB embedding load (`:304`).
* Compaction (`main/app/prometheus/compact.py:20-21`): episode token budget `8000`, episode cap `12`.
* Model: `gemini-flash-lite-latest`; chat temperature `0.5` (`agent.py:245,250`), memory extraction temperature `0.2` (`main/app/orunmila/memory.py:493,497`).
* `TOOL_REGISTRY` — 7 tools (`main/app/orunmila/tools.py:179-187`): `search_memory`, `save_memory`, `execute_code`, `read_file`, `write_file`, `list_files`, `serve_file`.
* ForgeVM sandbox per chat turn (`main/app/orunmila/sandbox.py:24-66`): `spawn(image, memory_mb, vcpus, ttl)` from `ORUNMILA_*` settings; per-user workspace under `/workspace/{userId}` with path-traversal guard (`hostPath`).
* Streaming over SSE via `sse_starlette` (`main/app/orunmila/stream_bus.py:6,109`): `JSONServerSentEvent` generator + `EventSourceResponse(..., ping=15)`.
* Memory (`main/app/orunmila/memory.py`): fused rank `0.6 * vector + 0.25 * fulltext + 0.15 * recency` (`:173`); caps 50 basic / 250 premium (`:72-73`); `rapidfuzz` dedup (`:134`); `cashews` cache matrix (`:30`); deferred BLOB embedding load (`:304`).
* Compaction (`main/app/orunmila/compact.py:20-21`): episode token budget `8000`, episode cap `12`.

## API Endpoints

Router prefix `/prometheus` (`main/controller/prometheus_controller.py`):

* `GET /prometheus/health` — liveness (`:31`).
* `GET /prometheus/sessions` — list user sessions, default `limit=20` (`:36`).
* `PUT /prometheus/sessions/{sessionId}` — rename (`:55`).
* `GET /prometheus/history/{sessionId}` — ownership-protected history (`:70`).
* `DELETE /prometheus/sessions/{sessionId}` — delete (`:89`).
* `POST /prometheus/chat/stream` — start SSE run, 5/min (`:101`).
* `GET /prometheus/chat/stream/{sessionId}` — resume SSE run (`:153`).
* `DELETE /prometheus/workspace/delete` — delete file, 30/min (`:164`).
* `GET /prometheus/workspace/download?path=` — download file (`:178`).
* `GET /prometheus/workspace/list?path=/workspace` — list files (`:194`).
Router prefix `/orunmila` (`main/controller/orunmila_controller.py`):

* `GET /orunmila/health` — liveness (`:31`).
* `GET /orunmila/sessions` — list user sessions, default `limit=20` (`:36`).
* `PUT /orunmila/sessions/{sessionId}` — rename (`:55`).
* `GET /orunmila/history/{sessionId}` — ownership-protected history (`:70`).
* `DELETE /orunmila/sessions/{sessionId}` — delete (`:89`).
* `POST /orunmila/chat/stream` — start SSE run, 5/min (`:101`).
* `GET /orunmila/chat/stream/{sessionId}` — resume SSE run (`:153`).
* `DELETE /orunmila/workspace/delete` — delete file, 30/min (`:164`).
* `GET /orunmila/workspace/download?path=` — download file (`:178`).
* `GET /orunmila/workspace/list?path=/workspace` — list files (`:194`).

## License

Expand Down
2 changes: 1 addition & 1 deletion docs/stocks_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ Mounted at `/stocks/mcp` via FastApiMCP (stocksapi_service.py:33-45) exposing 5
- **Cache build**: feather written in 2000-row streaming batches; lost DB connections retried 3x on `OperationalError` (cache.py:71-77). Cross-process `fcntl` build lock with no-op fallback (`tryBuildLock`, cache.py:263,168-182); build runs in a `subprocess` (`sys.executable -c ... buildFeatherCache()`, cache.py:270-277). Nested JSON columns keep a 20-row decompressed sample (cache.py:108-113). Frame sorted `TICKER` asc / `TIME` desc (cache.py:185-192). Refresh every 12h, stale threshold 6h with background rebuild (cache.py:36,220-226).
- **Abbreviations**: `generateAbbreviations` with `dedupAbbrev` (util.py:47-53), meta `TK/NM/TI`; nested fields auto-detected with URL subfields dropped in compact (`detectNestedFields`, util.py:96-130).
- **Compact wire form**: cotations → `{"h": "D,P", "d": [...]}` with `DD-MM` dates and `K/M/B/T` ints (compress.py:100-109); live → `PA/PO/PMN/PMX/PMD` (compress.py:11-16).
- **Transport/caching**: `GZipMiddleware(minimum_size=4096, compresslevel=3)` (service:31); endpoint cache is `cashews` over `mem://` (controller:17) with TTLs above; `/fields` fetch from Prometheus side also retried 3x on transient (compact.py:77).
- **Transport/caching**: `GZipMiddleware(minimum_size=4096, compresslevel=3)` (service:31); endpoint cache is `cashews` over `mem://` (controller:17) with TTLs above; `/fields` fetch from Orunmila side also retried 3x on transient (compact.py:77).

## License

Expand Down
Loading
Loading