Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
4d28f95
includes all the files when ventis build
Saaketh0 Aug 26, 2026
23e3928
fixed some bugs
Saaketh0 Aug 26, 2026
95240ca
ventis build: sweep project .py files into Docker build contexts
Saaketh0 Aug 26, 2026
69d5405
Fix missing os import in metrics_agent.py
Saaketh0 Aug 26, 2026
653bee8
WIP: OTel exporter testing + portfolio merge-conflict fix (pre-pull c…
Saaketh0 Aug 26, 2026
d861795
Merge origin/main into feature/otel-exporter
Saaketh0 Aug 26, 2026
4af43ae
[Feature] Pass env / secrets into agent containers
nickhuo Aug 26, 2026
087ee15
Harden the remote env file copy against a hostile /tmp
nickhuo Aug 26, 2026
db0ba26
WIP: OTel multi-destination fan-out (Railway+Langfuse+Grafana) + clea…
Saaketh0 Aug 27, 2026
1d319a5
Merge PR #51 (feature/all-the-files) into feature/otel-exporter
Saaketh0 Aug 27, 2026
098548c
Dedupe 'import os' from PR #51 merge (both sides added it independently)
Saaketh0 Aug 27, 2026
77231ec
Merge PR #53 (env_file secret injection) into feature/otel-exporter
Saaketh0 Aug 27, 2026
0b9546c
Fix PR #51 regression: disable entrypoint-based stub relocation
Saaketh0 Aug 27, 2026
7b3b167
Parallelize per-instance polling in _poll_controllers
Saaketh0 Aug 27, 2026
b5d6e4d
rough draft
Saaketh0 Aug 28, 2026
9bbc35d
rough draft
Saaketh0 Aug 28, 2026
8baed6c
Parallelize per-instance polling in _poll_controllers
Saaketh0 Aug 31, 2026
cd3a521
cleaned up OTel Exporter
Saaketh0 Aug 31, 2026
50733b8
Merge branch 'main' into feature/otel-exporter
Saaketh0 Aug 31, 2026
03d0a56
Merge feature/otel-exporter into improvement/global-polling
Saaketh0 Aug 31, 2026
1e6a6d8
Align with feature/otel-exporter: use updated langfuse config example…
Saaketh0 Aug 31, 2026
ede3fac
Restore parallelized polling implementation
Saaketh0 Aug 31, 2026
ea91ff9
added concurrent polling
Saaketh0 Aug 31, 2026
b4e45e8
Redis-backed otel destination reload: config change no longer needs f…
Saaketh0 Sep 3, 2026
9f630fe
Simplify: assume otel_exporter's Redis is always localhost:6379
Saaketh0 Sep 3, 2026
c085dbc
Trim explanatory comments off simple/obvious functions
Saaketh0 Sep 3, 2026
53a96c7
Ventis fixes extracted from the CLI packaging work
Saaketh0 Sep 4, 2026
0508422
Point example workflow imports at the stub's entrypoint path
Saaketh0 Sep 4, 2026
ff61053
Route local-provider agents over a dedicated docker network
Saaketh0 Sep 4, 2026
6f36d4f
Add the canyonos CLI package, porting skill, and .car examples
Saaketh0 Sep 4, 2026
7cbd976
removed some useless code
Saaketh0 Sep 4, 2026
c12c5c5
removed more useless code
Saaketh0 Sep 4, 2026
ced549d
cli
Saaketh0 Sep 4, 2026
9447e34
cleanup
Saaketh0 Sep 5, 2026
0931ffc
Integrate LLM proxy telemetry, replacing bedrock.py
Saaketh0 Sep 5, 2026
8f15161
Fix boto3 header injection: use before-call params['headers'] not req…
Saaketh0 Sep 5, 2026
d34df4e
docs: add cli/ARCHITECTURE.md and link it from cli/README.md
Saaketh0 Sep 5, 2026
6655c25
reviewed branch and code, made small changes
Saaketh0 Sep 8, 2026
ff33142
Merge origin/main into cli/llm-proxy
Saaketh0 Sep 9, 2026
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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ Edit `.car/config/global_controller.yaml` in your project directory to list the
Agents that need API keys read them from environment variables. Point `env_file` at a `.env` file to have Ventis inject it into every agent container:

```yaml
# config/global_controller.yaml
# .car/config/global_controller.yaml
env_file: .env
```

Expand Down
2 changes: 1 addition & 1 deletion cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ leaving every other line unchanged.

If you have a workflow running, and want to make a config change, canyonos config automatically would reload the project with your config. If you change the workflow files itself though and want the changes to take effect, you need to redeploy from scratch, running canyonos build for good measure

# Use: canyonos -h
# Use: canyonos -h
18 changes: 8 additions & 10 deletions examples/portfolio/agents/advisor_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
#
# Final stage. Turns the computed portfolio metrics and risk figures into a
# short, plain-English briefing using a small, cheap model on AWS Bedrock
# (Converse API), called via ventis.controller.bedrock so token/cost telemetry gets
# recorded onto this execution's future:<future_id> hash. Configure
# (Converse API), called directly via boto3. Token/cost telemetry is recorded
# onto this execution's future:<future_id> hash transparently by the Ventis LLM
# proxy each agent container's boto3 calls are routed through. Configure
# with env vars:
# BEDROCK_MODEL_ID (default: meta.llama3-8b-instruct-v1:0)
# AWS_REGION (default: us-east-1)
Expand All @@ -15,10 +16,7 @@

import os

try:
from ventis.controller.bedrock import call_bedrock
except ImportError:
from bedrock import call_bedrock
import boto3


class AdvisorAgent(object):
Expand All @@ -28,16 +26,16 @@ def __init__(self):
"BEDROCK_MODEL_ID", "meta.llama3-8b-instruct-v1:0"
)
self.region = os.environ.get("AWS_REGION", "us-east-1")
self._client = boto3.client("bedrock-runtime", region_name=self.region)

def summarize(self, holdings: dict, metrics: dict, risk: dict) -> str:
"""Write a short plain-English briefing on the portfolio."""
prompt = self._build_prompt(holdings, metrics, risk)
try:
response = call_bedrock(
model_id=self.model_id,
response = self._client.converse(
modelId=self.model_id,
messages=[{"role": "user", "content": [{"text": prompt}]}],
inference_config={"maxTokens": 400, "temperature": 0.2},
region=self.region,
inferenceConfig={"maxTokens": 400, "temperature": 0.2},
)
return response["output"]["message"]["content"][0]["text"]
except Exception as e:
Expand Down
21 changes: 10 additions & 11 deletions examples/portfolio/agents/intent_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
# -> {"holdings": {"AAPL": 0.4, "MSFT": 0.35, "NVDA": 0.25},
# "lookback_days": 180}
#
# Calls AWS Bedrock (Converse API) via ventis.controller.bedrock -- same pattern as
# AdvisorAgent -- so token/cost telemetry gets recorded onto this execution's
# future:<future_id> hash. Configure with env vars:
# Calls AWS Bedrock (Converse API) directly via boto3 -- same pattern as
# AdvisorAgent. Token/cost telemetry is recorded onto this execution's
# future:<future_id> hash transparently by the Ventis LLM proxy, which each
# agent container's boto3 calls are routed through (AWS_ENDPOINT_URL_BEDROCK_RUNTIME).
# Configure with env vars:
# BEDROCK_MODEL_ID (default: meta.llama3-8b-instruct-v1:0)
# AWS_REGION (default: us-east-1)
#
Expand All @@ -24,10 +26,7 @@
import re
import json

try:
from ventis.controller.bedrock import call_bedrock
except ImportError:
from bedrock import call_bedrock
import boto3

DEFAULT_LOOKBACK_DAYS = 365

Expand All @@ -39,14 +38,14 @@ def __init__(self):
"BEDROCK_MODEL_ID", "meta.llama3-8b-instruct-v1:0"
)
self.region = os.environ.get("AWS_REGION", "us-east-1")
self._client = boto3.client("bedrock-runtime", region_name=self.region)

def parse(self, query: str) -> dict:
"""Parse a natural-language portfolio request into holdings + lookback."""
response = call_bedrock(
model_id=self.model_id,
response = self._client.converse(
modelId=self.model_id,
messages=[{"role": "user", "content": [{"text": self._build_prompt(query)}]}],
inference_config={"maxTokens": 300, "temperature": 0.0},
region=self.region,
inferenceConfig={"maxTokens": 300, "temperature": 0.0},
)
text = response["output"]["message"]["content"][0]["text"]
if not text:
Expand Down
2 changes: 1 addition & 1 deletion examples/portfolio/config/global_controller.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

agents:
# Stage 0: parse the free-text request into structured holdings + lookback
# window (calls Bedrock directly via ventis.llm.bedrock). Cheap CPU, one
# window (calls Bedrock via boto3, routed through the Ventis LLM proxy). Cheap CPU, one
# call per request, on the critical path before the fan-out.
- name: IntentAgent
redis_port: 6379
Expand Down
20 changes: 9 additions & 11 deletions examples/text2sql/agents/vllm_agent.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# VLLM Agent
#
# LLM backend for SQL candidate generation, called remotely by
# SQLGeneratorAgent. Calls AWS Bedrock (Converse API) via ventis.controller.bedrock
# so token/cost telemetry gets recorded onto this execution's
# future:<future_id> hash — same pattern as
# SQLGeneratorAgent. Calls AWS Bedrock (Converse API) directly via boto3.
# Token/cost telemetry is recorded onto this execution's future:<future_id>
# hash transparently by the Ventis LLM proxy each agent container's boto3 calls
# are routed through — same pattern as
# examples/portfolio/agents/advisor_agent.py.
# Configure with env vars:
# BEDROCK_MODEL_ID (default: meta.llama3-8b-instruct-v1:0)
Expand All @@ -13,26 +14,23 @@

import os

try:
from ventis.controller.bedrock import call_bedrock
except ImportError:
from bedrock import call_bedrock
import boto3


class VllmAgent(object):
def __init__(self):
self.tools = [self.generate]
self.model_id = os.environ.get("BEDROCK_MODEL_ID", "meta.llama3-8b-instruct-v1:0")
self.region = os.environ.get("AWS_REGION", "us-east-1")
self._client = boto3.client("bedrock-runtime", region_name=self.region)

def generate(self, prompt: str) -> str:
"""Generates a response using an LLM model based on the given prompt."""
try:
response = call_bedrock(
model_id=self.model_id,
response = self._client.converse(
modelId=self.model_id,
messages=[{"role": "user", "content": [{"text": prompt}]}],
inference_config={"maxTokens": 400, "temperature": 0.2},
region=self.region,
inferenceConfig={"maxTokens": 400, "temperature": 0.2},
)
return response["output"]["message"]["content"][0]["text"]
except Exception as e:
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ dependencies = [
"psycopg[binary]",
"pyyaml",
"flask",
"requests",
"psutil",
"opentelemetry-api>=1.44.0",
"opentelemetry-sdk>=1.44.0",
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ grpcio-tools
redis
pyyaml
flask
requests
sqlalchemy
psycopg[binary]
psutil
Expand Down
4 changes: 4 additions & 0 deletions tests/test_instance_manager_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@ def test_local_instances_keep_default_host_and_increment_host_ports(self):
"VENTIS_REDIS_PORT=6379",
"-e",
"VENTIS_POLL_INTERVAL=5",
"-e",
"AWS_ENDPOINT_URL_BEDROCK_RUNTIME=http://127.0.0.1:8081/bedrock",
"ventis-alpha",
],
"localhost",
Expand Down Expand Up @@ -226,6 +228,8 @@ def test_local_workflow_and_resource_flags_stay_the_same(self):
"VENTIS_REDIS_PORT=6379",
"-e",
"VENTIS_POLL_INTERVAL=5",
"-e",
"AWS_ENDPOINT_URL_BEDROCK_RUNTIME=http://127.0.0.1:8081/bedrock",
"-p",
"8080:8080",
"--cpus",
Expand Down
2 changes: 2 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 8 additions & 8 deletions ventis/FUTURE_SCHEMA.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,16 @@ Fields currently written into `future:{future_id}`, and where:
| `created_at` | `future.py` only (origin submission time) |
| `result` | `future.py`, `local_controller.py` |
| `failed` | `future.py`, `local_controller.py` |
| `error` | `future.py` (`_submit_request`), `local_controller.py` (`_mark_future_failed`) -- the sole failure-message field; `bedrock.py` deliberately never writes it |
| `error` | `future.py` (`_submit_request`), `local_controller.py` (`_mark_future_failed`) -- the sole failure-message field; the LLM proxy deliberately never writes it |
| `finished_at` | `local_controller.py` (`_execute_locally` finally block) |
| `cpu_resource` | `local_controller.py` |
| `gpu_resource` | `local_controller.py` |
| `agent` | `local_controller.py` (agent_id that executed this step) |
| `queue_time` | `local_controller.py` (only when `submitted_at` is known) |
| `model` | `llm/bedrock.py` (`call_bedrock`) |
| `input_token_count` | `llm/bedrock.py` |
| `output_token_count` | `llm/bedrock.py` |
| `token_count` | `llm/bedrock.py` |
| `errors` | `llm/bedrock.py` (Bedrock call error count) |
| `input_cache_tokens` | `llm/bedrock.py` |
| `input_cache_write_tokens` | `llm/bedrock.py` |
| `model` | `llm_proxy/hooks.py` (on_response) |
| `input_token_count` | `llm_proxy/hooks.py` |
| `output_token_count` | `llm_proxy/hooks.py` |
| `token_count` | `llm_proxy/hooks.py` |
| `errors` | `llm_proxy/hooks.py` (Bedrock call error flag) |
| `input_cache_tokens` | `llm_proxy/hooks.py` |
| `input_cache_write_tokens` | `llm_proxy/hooks.py` |
52 changes: 0 additions & 52 deletions ventis/controller/bedrock.py

This file was deleted.

4 changes: 4 additions & 0 deletions ventis/controller/cloud_provider_logic/EC2/_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,10 @@ def _bootstrap_instance(host, spec, replica_index, cfg, redis_host, redis_port,
f"VENTIS_AGENT_PORT={CONTAINER_PORT}",
"-e",
f"VENTIS_POLL_INTERVAL={_controller.config.get('poll_interval', 5)}",
# Route the agent's boto3 Bedrock calls through the in-container LLM
# proxy (started by LocalController) so token/cost telemetry is captured.
"-e",
"AWS_ENDPOINT_URL_BEDROCK_RUNTIME=http://127.0.0.1:8081/bedrock",
]
if spec.get("type") == "workflow":
db_url = _controller.config.get("database", {}).get("url")
Expand Down
4 changes: 4 additions & 0 deletions ventis/controller/cloud_provider_logic/Local/_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ def bootstrap_instance(provisioned, spec, replica_index, agent_id):
f"VENTIS_REDIS_PORT={spec.get('redis_port', 6379)}",
"-e",
f"VENTIS_POLL_INTERVAL={_require_controller().config.get('poll_interval', 5)}",
# Route the agent's boto3 Bedrock calls through the in-container LLM
# proxy (started by LocalController) so token/cost telemetry is captured.
"-e",
"AWS_ENDPOINT_URL_BEDROCK_RUNTIME=http://127.0.0.1:8081/bedrock",
]
if ctrl_type == "workflow":
cmd.extend(["-p", f"{spec.get('api_port', 8080)}:8080"])
Expand Down
44 changes: 44 additions & 0 deletions ventis/controller/local_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@
import ventis.controller.ventis_context as ventis_context
except ImportError:
import ventis_context

# Auto-inject X-Ventis-Future-ID into all boto3 Bedrock calls so the LLM proxy
# can attribute token/cost telemetry to the executing future. Import for its
# global boto3 event-hook side effect; safe no-op if the proxy isn't present.
try:
from ventis.llm_proxy import proxy as _llm_proxy_autoinject # noqa: F401
except ImportError:
try:
from llm_proxy import proxy as _llm_proxy_autoinject # noqa: F401
except ImportError:
pass # No proxy available; agents call Bedrock directly.

import local_controler_pb2
import local_controler_pb2_grpc

Expand Down Expand Up @@ -102,6 +114,11 @@ def __init__(self, port=50051):
max_instances = int(os.environ.get("VENTIS_MAX_AGENT_INSTANCES", 8))
self._executor = ThreadPoolExecutor(max_workers=max_instances)

# Start the LLM proxy alongside the agent in this container. Bedrock
# calls are routed to it via AWS_ENDPOINT_URL_BEDROCK_RUNTIME (injected
# by the runtime), and it writes token/cost telemetry to Redis.
self._proxy_process = self._start_llm_proxy(redis_host, redis_port)

logger.info(
"Local controller initialized at %s (max_agent_instances=%d), reported healthy to Redis.",
self._my_endpoint,
Expand All @@ -111,6 +128,33 @@ def __init__(self, port=50051):
# Load the agent class dynamically
self.agent = self._load_agent()

def _start_llm_proxy(self, redis_host, redis_port):
"""Start the LLM proxy as a subprocess in this container (127.0.0.1:8081).

Best-effort: a failure here must never stop the controller from coming up.
"""
import subprocess

try:
proxy_env = os.environ.copy()
proxy_env.update({
"PROXY_HOST": "127.0.0.1",
"PROXY_PORT": "8081",
"VENTIS_REDIS_HOST": redis_host,
"VENTIS_REDIS_PORT": str(redis_port),
})
proxy_process = subprocess.Popen(
[sys.executable, "-m", "ventis.llm_proxy"],
env=proxy_env,
)
logger.info(
"Started LLM proxy on 127.0.0.1:8081 (PID: %d)", proxy_process.pid
)
return proxy_process
except Exception as e:
logger.warning("Failed to start LLM proxy: %s", e)
return None

def _collect_metrics(self):
"""Snapshot current instance health/resource metrics.

Expand Down
Loading
Loading