nexus-agent-framework 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. nexus/__init__.py +3 -0
  2. nexus/__main__.py +4 -0
  3. nexus/agents/__init__.py +6 -0
  4. nexus/agents/base.py +93 -0
  5. nexus/agents/context.py +45 -0
  6. nexus/ai/__init__.py +18 -0
  7. nexus/ai/client.py +390 -0
  8. nexus/ai/pricing.py +28 -0
  9. nexus/api/__init__.py +14 -0
  10. nexus/api/app.py +119 -0
  11. nexus/api/middleware.py +33 -0
  12. nexus/cli/__init__.py +5 -0
  13. nexus/cli/__main__.py +4 -0
  14. nexus/cli/main.py +324 -0
  15. nexus/dashboard/__init__.py +17 -0
  16. nexus/dashboard/server.py +377 -0
  17. nexus/dashboard/telemetry.py +92 -0
  18. nexus/database/__init__.py +5 -0
  19. nexus/db/__init__.py +14 -0
  20. nexus/db/base.py +37 -0
  21. nexus/db/session.py +51 -0
  22. nexus/maintenance/__init__.py +5 -0
  23. nexus/maintenance/healer.py +38 -0
  24. nexus/orchestrator/__init__.py +6 -0
  25. nexus/orchestrator/mcp.py +112 -0
  26. nexus/orchestrator/protocol.py +54 -0
  27. nexus/providers/__init__.py +7 -0
  28. nexus/providers/base.py +77 -0
  29. nexus/providers/mock.py +85 -0
  30. nexus/providers/vertex.py +91 -0
  31. nexus/qa/__init__.py +5 -0
  32. nexus/qa/judge.py +38 -0
  33. nexus/scraping/__init__.py +5 -0
  34. nexus/scraping/client.py +86 -0
  35. nexus/security/__init__.py +12 -0
  36. nexus/security/guardrail.py +45 -0
  37. nexus/security/injection.py +32 -0
  38. nexus/security/sanitizer.py +25 -0
  39. nexus/teams/__init__.py +5 -0
  40. nexus/teams/it_pack/__init__.py +32 -0
  41. nexus/teams/it_pack/ba.py +48 -0
  42. nexus/teams/it_pack/data_architect.py +45 -0
  43. nexus/teams/it_pack/designer.py +43 -0
  44. nexus/teams/it_pack/devops.py +53 -0
  45. nexus/teams/it_pack/engineer.py +41 -0
  46. nexus/teams/it_pack/fullstack.py +65 -0
  47. nexus/teams/it_pack/manager.py +55 -0
  48. nexus/teams/it_pack/qa.py +56 -0
  49. nexus/teams/it_pack/solution_architect.py +60 -0
  50. nexus/teams/it_pack/team.py +422 -0
  51. nexus/tools/__init__.py +5 -0
  52. nexus/tools/base.py +82 -0
  53. nexus_agent_framework-0.1.0.dist-info/METADATA +201 -0
  54. nexus_agent_framework-0.1.0.dist-info/RECORD +56 -0
  55. nexus_agent_framework-0.1.0.dist-info/WHEEL +4 -0
  56. nexus_agent_framework-0.1.0.dist-info/entry_points.txt +2 -0
nexus/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Nexus Framework: Unified application core for Google Cloud and Gemini 3.8."""
2
+
3
+ __version__ = "0.1.0"
nexus/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from nexus.cli.main import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,6 @@
1
+ """Nexus Agents: Autonomous agent primitives, context, and execution runtime."""
2
+
3
+ from nexus.agents.base import AgentRunResult, BaseAgent
4
+ from nexus.agents.context import AgentContext, BudgetExceededError
5
+
6
+ __all__ = ["AgentContext", "AgentRunResult", "BaseAgent", "BudgetExceededError"]
nexus/agents/base.py ADDED
@@ -0,0 +1,93 @@
1
+ """Nexus Agents: Base Autonomous Agent Primitive."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from collections.abc import Sequence
7
+
8
+ from nexus.agents.context import AgentContext
9
+ from nexus.providers.base import BaseModelProvider
10
+ from nexus.providers.vertex import VertexAIProvider
11
+ from nexus.security.guardrail import GuardrailManager
12
+ from nexus.tools.base import Tool
13
+
14
+ logger = logging.getLogger("nexus.agents")
15
+
16
+
17
+ class AgentRunResult:
18
+ """The result of an autonomous agent task execution."""
19
+
20
+ def __init__(
21
+ self,
22
+ output: str,
23
+ turns: int,
24
+ context: AgentContext,
25
+ ) -> None:
26
+ self.output = output
27
+ self.turns = turns
28
+ self.context = context
29
+ self.total_cost_usd = context.accumulated_cost_usd
30
+ self.input_tokens = context.total_input_tokens
31
+ self.output_tokens = context.total_output_tokens
32
+
33
+ def __repr__(self) -> str:
34
+ return f"<AgentRunResult output={self.output[:40]!r}... turns={self.turns} cost=${self.total_cost_usd:.6f}>"
35
+
36
+
37
+ class BaseAgent:
38
+ """
39
+ Autonomous AI Agent Primitive.
40
+ Handles reasoning, tool dispatching, security guardrails, and budget enforcement.
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ name: str,
46
+ system_prompt: str,
47
+ provider: BaseModelProvider | None = None,
48
+ tools: Sequence[Tool] | None = None,
49
+ guardrail: GuardrailManager | None = None,
50
+ max_budget_usd: float = 0.50,
51
+ max_turns: int = 6,
52
+ ) -> None:
53
+ self.name = name
54
+ self.system_prompt = system_prompt
55
+ self.provider = provider or VertexAIProvider()
56
+ self.tools = {t.name: t for t in (tools or [])}
57
+ self.guardrail = guardrail or GuardrailManager()
58
+ self.max_budget_usd = max_budget_usd
59
+ self.max_turns = max_turns
60
+
61
+ def run(self, task: str) -> AgentRunResult:
62
+ """Execute a task autonomously."""
63
+ # 1. Inspect and scrub input via security guardrails
64
+ clean_task = self.guardrail.inspect_input(task)
65
+
66
+ # 2. Initialize execution context with spend limit
67
+ context = AgentContext(max_budget_usd=self.max_budget_usd)
68
+ context.add_user_message(clean_task)
69
+
70
+ tool_schemas = [t.schema for t in self.tools.values()]
71
+ turn_count = 0
72
+
73
+ for turn in range(self.max_turns):
74
+ turn_count += 1
75
+ # Execute model turn
76
+ resp = self.provider.converse(
77
+ messages=context.messages,
78
+ tools=tool_schemas,
79
+ system_instruction=self.system_prompt,
80
+ )
81
+ context.record_usage(resp.input_tokens, resp.output_tokens, resp.cost_usd)
82
+
83
+ # Check if tools were executed or final answer returned
84
+ # If plain text returned
85
+ if resp.text:
86
+ context.add_model_message(resp.text)
87
+ return AgentRunResult(output=resp.text, turns=turn_count, context=context)
88
+
89
+ return AgentRunResult(
90
+ output="Agent completed max turns without final message.",
91
+ turns=turn_count,
92
+ context=context,
93
+ )
@@ -0,0 +1,45 @@
1
+ """Nexus Agents: Execution Context, memory, and budget tracking."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+ from typing import Any
7
+
8
+
9
+ class BudgetExceededError(Exception):
10
+ """Raised when an agent session hits its spend ceiling."""
11
+
12
+
13
+ class AgentContext:
14
+ """Manages working memory, message logs, and spend ledger for an agent run."""
15
+
16
+ def __init__(
17
+ self,
18
+ session_id: str | None = None,
19
+ max_budget_usd: float = 0.50, # Default safety cap: $0.50 max per run
20
+ ) -> None:
21
+ self.session_id = session_id or str(uuid.uuid4())
22
+ self.max_budget_usd = max_budget_usd
23
+ self.messages: list[dict[str, Any]] = []
24
+ self.tool_executions: list[dict[str, Any]] = []
25
+ self.accumulated_cost_usd: float = 0.0
26
+ self.total_input_tokens: int = 0
27
+ self.total_output_tokens: int = 0
28
+
29
+ def record_usage(self, input_tokens: int, output_tokens: int, cost_usd: float) -> None:
30
+ """Record model inference tokens and check budget ceiling."""
31
+ self.total_input_tokens += input_tokens
32
+ self.total_output_tokens += output_tokens
33
+ self.accumulated_cost_usd += cost_usd
34
+
35
+ if self.accumulated_cost_usd > self.max_budget_usd:
36
+ raise BudgetExceededError(
37
+ f"Agent session {self.session_id} exceeded max budget of ${self.max_budget_usd:.4f} "
38
+ f"(current cost: ${self.accumulated_cost_usd:.4f})"
39
+ )
40
+
41
+ def add_user_message(self, text: str) -> None:
42
+ self.messages.append({"role": "user", "parts": [{"text": text}]})
43
+
44
+ def add_model_message(self, text: str) -> None:
45
+ self.messages.append({"role": "model", "parts": [{"text": text}]})
nexus/ai/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ """Nexus AI package: Vertex AI & Gemini 3.8 client, pricing, and structured output."""
2
+
3
+ from nexus.ai.client import (
4
+ GeminiClient,
5
+ GeminiResponse,
6
+ get_gcp_access_token,
7
+ get_gcp_project,
8
+ )
9
+ from nexus.ai.pricing import DEFAULT_MODEL, calculate_cost_usd
10
+
11
+ __all__ = [
12
+ "DEFAULT_MODEL",
13
+ "GeminiClient",
14
+ "GeminiResponse",
15
+ "calculate_cost_usd",
16
+ "get_gcp_access_token",
17
+ "get_gcp_project",
18
+ ]
nexus/ai/client.py ADDED
@@ -0,0 +1,390 @@
1
+ """Nexus AI: Standardized Google Cloud Vertex AI & Gemini 3.8 Client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ import os
8
+ import shutil
9
+ import subprocess
10
+ import time
11
+ from collections.abc import Callable, Mapping, Sequence
12
+ from typing import Any, TypeVar
13
+
14
+ import httpx
15
+ from pydantic import BaseModel
16
+
17
+ from nexus.ai.pricing import DEFAULT_MODEL, calculate_cost_usd
18
+
19
+ logger = logging.getLogger("nexus.ai")
20
+
21
+ # Cache token locally for 40 minutes to prevent overhead
22
+ _CACHED_TOKEN: tuple[str, float] | None = None
23
+ _TOKEN_TTL_SECONDS = 40 * 60
24
+
25
+ T = TypeVar("T", bound=BaseModel)
26
+
27
+
28
+ def get_gcp_project() -> str:
29
+ """Resolve the active Google Cloud project ID."""
30
+ proj = os.getenv("GCP_PROJECT_ID") or os.getenv("GOOGLE_CLOUD_PROJECT")
31
+ if proj:
32
+ return proj
33
+ gcloud_bin = shutil.which("gcloud") or shutil.which("gcloud.cmd")
34
+ if gcloud_bin:
35
+ try:
36
+ res = subprocess.run(
37
+ [gcloud_bin, "config", "get-value", "project"],
38
+ capture_output=True,
39
+ text=True,
40
+ timeout=5,
41
+ check=False,
42
+ )
43
+ val = res.stdout.strip()
44
+ if val and val != "(unset)":
45
+ return val
46
+ except Exception:
47
+ pass
48
+ return "finhub-510316"
49
+
50
+
51
+ def get_gcp_access_token() -> str | None:
52
+ """Acquire an OAuth access token via IAM instance metadata or gcloud CLI."""
53
+ global _CACHED_TOKEN
54
+ now = time.time()
55
+ if _CACHED_TOKEN and (now - _CACHED_TOKEN[1]) < _TOKEN_TTL_SECONDS:
56
+ return _CACHED_TOKEN[0]
57
+
58
+ # 1. Environment token override
59
+ env_token = os.getenv("GCP_ACCESS_TOKEN") or os.getenv("GOOGLE_OAUTH_ACCESS_TOKEN")
60
+ if env_token:
61
+ _CACHED_TOKEN = (env_token.strip(), now)
62
+ return env_token.strip()
63
+
64
+ # 2. Check GCP Compute Engine / Cloud Run instance metadata server
65
+ try:
66
+ resp = httpx.get(
67
+ "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token",
68
+ headers={"Metadata-Flavor": "Google"},
69
+ timeout=1.5,
70
+ )
71
+ if resp.status_code == 200:
72
+ token = resp.json().get("access_token")
73
+ if token:
74
+ _CACHED_TOKEN = (token, now)
75
+ return str(token)
76
+ except Exception:
77
+ pass
78
+
79
+ # 3. Local development fallback: gcloud auth print-access-token
80
+ gcloud_bin = shutil.which("gcloud") or shutil.which("gcloud.cmd")
81
+ if gcloud_bin:
82
+ try:
83
+ res = subprocess.run(
84
+ [gcloud_bin, "auth", "print-access-token"],
85
+ capture_output=True,
86
+ text=True,
87
+ timeout=15,
88
+ check=False,
89
+ )
90
+ token = res.stdout.strip()
91
+ if token and res.returncode == 0 and not token.startswith("ERROR"):
92
+ _CACHED_TOKEN = (token, now)
93
+ return token
94
+ except Exception as exc:
95
+ logger.debug("Local gcloud token lookup failed: %s", exc)
96
+
97
+ return None
98
+
99
+
100
+ class GeminiResponse:
101
+ """Normalized response from Gemini 3.8."""
102
+
103
+ def __init__(
104
+ self,
105
+ text: str,
106
+ input_tokens: int,
107
+ output_tokens: int,
108
+ cost_usd: float,
109
+ model: str,
110
+ raw: Mapping[str, Any],
111
+ ) -> None:
112
+ self.text = text
113
+ self.input_tokens = input_tokens
114
+ self.output_tokens = output_tokens
115
+ self.cost_usd = cost_usd
116
+ self.model = model
117
+ self.raw = raw
118
+
119
+ def __repr__(self) -> str:
120
+ return f"<GeminiResponse model={self.model} cost=${self.cost_usd:.6f} length={len(self.text)}>"
121
+
122
+
123
+ class GeminiClient:
124
+ """
125
+ Standardized client for Google Cloud Vertex AI & Gemini 3.8 Flash.
126
+ Provides structured schema extraction, multimodal parsing, and multi-turn tool loops.
127
+ """
128
+
129
+ def __init__(
130
+ self,
131
+ project_id: str | None = None,
132
+ location: str = "global",
133
+ api_key: str | None = None,
134
+ default_model: str = DEFAULT_MODEL,
135
+ ) -> None:
136
+ self.project_id = project_id or get_gcp_project()
137
+ self.location = location
138
+ self.api_key = api_key or os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY")
139
+ self.default_model = default_model
140
+ self._http = httpx.Client(timeout=httpx.Timeout(180.0, connect=30.0, read=180.0))
141
+
142
+ def _post(
143
+ self,
144
+ url: str,
145
+ headers: dict[str, str],
146
+ body: dict[str, Any],
147
+ max_retries: int = 2,
148
+ ) -> httpx.Response:
149
+ import time
150
+
151
+ for attempt in range(max_retries):
152
+ try:
153
+ return self._http.post(url, headers=headers, json=body)
154
+ except (httpx.ReadTimeout, httpx.ConnectTimeout):
155
+ if attempt == max_retries - 1:
156
+ raise
157
+ time.sleep(2.0)
158
+ raise RuntimeError("HTTP request failed after max retries")
159
+
160
+ def _build_url_and_headers(self, model: str) -> tuple[str, dict[str, str]]:
161
+ token = get_gcp_access_token()
162
+ if token:
163
+ # Vertex AI global or regional publisher endpoint
164
+ if self.location == "global":
165
+ url = (
166
+ f"https://aiplatform.googleapis.com/v1/projects/{self.project_id}"
167
+ f"/locations/global/publishers/google/models/{model}:generateContent"
168
+ )
169
+ else:
170
+ url = (
171
+ f"https://{self.location}-aiplatform.googleapis.com/v1/projects/{self.project_id}"
172
+ f"/locations/{self.location}/publishers/google/models/{model}:generateContent"
173
+ )
174
+ headers = {
175
+ "Authorization": f"Bearer {token}",
176
+ "Content-Type": "application/json",
177
+ }
178
+ return url, headers
179
+
180
+ if self.api_key:
181
+ # Direct Google Generative Language API
182
+ url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={self.api_key}"
183
+ headers = {"Content-Type": "application/json"}
184
+ return url, headers
185
+
186
+ raise RuntimeError(
187
+ "No Google Cloud authentication found. Provide GCP IAM credentials, run "
188
+ "'gcloud auth application-default login', or set GEMINI_API_KEY."
189
+ )
190
+
191
+ def generate(
192
+ self,
193
+ prompt: str,
194
+ system_instruction: str | None = None,
195
+ model: str | None = None,
196
+ temperature: float = 0.2,
197
+ ) -> GeminiResponse:
198
+ """Execute a text completion turn."""
199
+ chosen_model = model or self.default_model
200
+ url, headers = self._build_url_and_headers(chosen_model)
201
+
202
+ body: dict[str, Any] = {
203
+ "contents": [{"role": "user", "parts": [{"text": prompt}]}],
204
+ "generationConfig": {
205
+ "temperature": temperature,
206
+ "thinkingConfig": {"thinkingLevel": "low"},
207
+ },
208
+ }
209
+ if system_instruction:
210
+ body["systemInstruction"] = {"parts": [{"text": system_instruction}]}
211
+
212
+ resp = self._post(url, headers=headers, body=body)
213
+ if resp.status_code != 200:
214
+ raise RuntimeError(f"Gemini API returned status {resp.status_code}: {resp.text}")
215
+
216
+ data = resp.json()
217
+ usage = data.get("usageMetadata", {})
218
+ inp = int(usage.get("promptTokenCount", 0))
219
+ out = int(usage.get("candidatesTokenCount", 0))
220
+ cost = calculate_cost_usd(inp, out, chosen_model)
221
+
222
+ candidates = data.get("candidates", [])
223
+ text = ""
224
+ if candidates:
225
+ parts = candidates[0].get("content", {}).get("parts", [])
226
+ for p in parts:
227
+ if "text" in p:
228
+ text += p["text"]
229
+
230
+ return GeminiResponse(
231
+ text=text.strip(),
232
+ input_tokens=inp,
233
+ output_tokens=out,
234
+ cost_usd=cost,
235
+ model=chosen_model,
236
+ raw=data,
237
+ )
238
+
239
+ def structured(
240
+ self,
241
+ prompt: str,
242
+ response_model: type[T],
243
+ system_instruction: str | None = None,
244
+ inline_pdf_bytes: bytes | None = None,
245
+ model: str | None = None,
246
+ ) -> T:
247
+ """Extract a strongly typed Pydantic schema using constrained JSON generation."""
248
+ chosen_model = model or self.default_model
249
+ url, headers = self._build_url_and_headers(chosen_model)
250
+
251
+ parts: list[dict[str, Any]] = []
252
+ if inline_pdf_bytes:
253
+ import base64
254
+
255
+ b64 = base64.b64encode(inline_pdf_bytes).decode("ascii")
256
+ parts.append({"inlineData": {"mimeType": "application/pdf", "data": b64}})
257
+ parts.append({"text": prompt})
258
+
259
+ # Generate JSON Schema from Pydantic model
260
+ schema = response_model.model_json_schema()
261
+ # Clean $defs / titles if needed for Gemini schema compatibility
262
+ clean_schema = self._sanitize_schema(schema)
263
+
264
+ body: dict[str, Any] = {
265
+ "contents": [{"role": "user", "parts": parts}],
266
+ "generationConfig": {
267
+ "temperature": 0.0,
268
+ "responseMimeType": "application/json",
269
+ "responseSchema": clean_schema,
270
+ "thinkingConfig": {"thinkingLevel": "low"},
271
+ },
272
+ }
273
+ if system_instruction:
274
+ body["systemInstruction"] = {"parts": [{"text": system_instruction}]}
275
+
276
+ resp = self._post(url, headers=headers, body=body)
277
+ if resp.status_code != 200:
278
+ raise RuntimeError(f"Gemini structured output failed {resp.status_code}: {resp.text}")
279
+
280
+ data = resp.json()
281
+ candidates = data.get("candidates", [])
282
+ if not candidates:
283
+ raise RuntimeError(f"No candidates returned in structured output: {data}")
284
+
285
+ parts_out = candidates[0].get("content", {}).get("parts", [])
286
+ raw_text = "".join(p.get("text", "") for p in parts_out).strip()
287
+ parsed_json = json.loads(raw_text)
288
+ return response_model.model_validate(parsed_json)
289
+
290
+ def converse(
291
+ self,
292
+ messages: Sequence[dict[str, Any]],
293
+ tools: Sequence[Callable[..., Any]] | Sequence[dict[str, Any]],
294
+ system_instruction: str | None = None,
295
+ max_turns: int = 5,
296
+ model: str | None = None,
297
+ ) -> GeminiResponse:
298
+ """Multi-turn tool-calling loop executing tools locally without hallucinations."""
299
+ chosen_model = model or self.default_model
300
+ url, headers = self._build_url_and_headers(chosen_model)
301
+
302
+ # Convert tools to function declarations
303
+ func_declarations: list[dict[str, Any]] = []
304
+ tool_dispatch: dict[str, Callable[..., Any]] = {}
305
+ for t in tools:
306
+ if callable(t):
307
+ fn_name = getattr(t, "__name__", "tool")
308
+ doc = getattr(t, "__doc__", "") or f"Execute {fn_name}"
309
+ tool_dispatch[fn_name] = t
310
+ func_declarations.append(
311
+ {
312
+ "name": fn_name,
313
+ "description": doc.strip(),
314
+ "parameters": {"type": "object", "properties": {}},
315
+ }
316
+ )
317
+ elif isinstance(t, dict):
318
+ func_declarations.append(t)
319
+
320
+ current_contents = list(messages)
321
+ total_inp = 0
322
+ total_out = 0
323
+
324
+ for _ in range(max_turns):
325
+ body: dict[str, Any] = {
326
+ "contents": current_contents,
327
+ "generationConfig": {
328
+ "temperature": 0.1,
329
+ "thinkingConfig": {"thinkingLevel": "low"},
330
+ },
331
+ }
332
+ if func_declarations:
333
+ body["tools"] = [{"functionDeclarations": func_declarations}]
334
+ if system_instruction:
335
+ body["systemInstruction"] = {"parts": [{"text": system_instruction}]}
336
+
337
+ resp = self._post(url, headers=headers, body=body)
338
+ if resp.status_code != 200:
339
+ raise RuntimeError(f"Gemini converse error {resp.status_code}: {resp.text}")
340
+
341
+ data = resp.json()
342
+ usage = data.get("usageMetadata", {})
343
+ total_inp += int(usage.get("promptTokenCount", 0))
344
+ total_out += int(usage.get("candidatesTokenCount", 0))
345
+
346
+ candidate = (data.get("candidates") or [{}])[0]
347
+ content = candidate.get("content", {})
348
+ parts = content.get("parts", [])
349
+
350
+ # Check if model requested tool call
351
+ function_calls = [p["functionCall"] for p in parts if "functionCall" in p]
352
+ if not function_calls:
353
+ # Text answer completed
354
+ text = "".join(p.get("text", "") for p in parts).strip()
355
+ cost = calculate_cost_usd(total_inp, total_out, chosen_model)
356
+ return GeminiResponse(text, total_inp, total_out, cost, chosen_model, data)
357
+
358
+ # Append model turn to conversation
359
+ current_contents.append({"role": "model", "parts": parts})
360
+
361
+ # Execute tool and append functionResponse
362
+ response_parts = []
363
+ for call in function_calls:
364
+ name = call.get("name", "")
365
+ args = call.get("args", {})
366
+ fn = tool_dispatch.get(name)
367
+ try:
368
+ result = fn(**args) if fn else {"status": "error", "message": f"Tool {name} not found"}
369
+ except Exception as exc:
370
+ result = {"status": "error", "message": str(exc)}
371
+
372
+ response_parts.append(
373
+ {
374
+ "functionResponse": {
375
+ "name": name,
376
+ "response": {"output": result if isinstance(result, (dict, list)) else str(result)},
377
+ }
378
+ }
379
+ )
380
+ current_contents.append({"role": "user", "parts": response_parts})
381
+
382
+ raise RuntimeError(f"Gemini converse exceeded max_turns={max_turns}")
383
+
384
+ @staticmethod
385
+ def _sanitize_schema(schema: dict[str, Any]) -> dict[str, Any]:
386
+ """Strip keywords unsupported by Gemini JSON Schema parser."""
387
+ s: dict[str, Any] = dict(json.loads(json.dumps(schema)))
388
+ for key in ("title", "$defs", "$schema", "default"):
389
+ s.pop(key, None)
390
+ return s
nexus/ai/pricing.py ADDED
@@ -0,0 +1,28 @@
1
+ """Token pricing and spend tracking for Vertex AI models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Final
6
+
7
+ # Vertex AI Gemini 3.8 Flash pricing per million tokens
8
+ GEMINI_3_8_FLASH_INPUT_PER_M: Final[float] = 0.075
9
+ GEMINI_3_8_FLASH_OUTPUT_PER_M: Final[float] = 0.30
10
+
11
+ # Fallback default model
12
+ DEFAULT_MODEL: Final[str] = "gemini-3.8-flash"
13
+
14
+
15
+ def calculate_cost_usd(
16
+ input_tokens: int,
17
+ output_tokens: int,
18
+ model: str = DEFAULT_MODEL,
19
+ ) -> float:
20
+ """Calculate the estimated USD cost for a model call."""
21
+ if "gemini-3.8" in model or "gemini-3" in model:
22
+ in_cost = (input_tokens / 1_000_000.0) * GEMINI_3_8_FLASH_INPUT_PER_M
23
+ out_cost = (output_tokens / 1_000_000.0) * GEMINI_3_8_FLASH_OUTPUT_PER_M
24
+ return in_cost + out_cost
25
+ # Standard Gemini 2.x / 1.5 pricing baseline
26
+ in_cost = (input_tokens / 1_000_000.0) * 0.15
27
+ out_cost = (output_tokens / 1_000_000.0) * 0.60
28
+ return in_cost + out_cost
nexus/api/__init__.py ADDED
@@ -0,0 +1,14 @@
1
+ """Nexus API package: Application factory and middleware."""
2
+
3
+ from nexus.ai.client import GeminiClient
4
+ from nexus.api.app import create_nexus_app
5
+ from nexus.api.middleware import NexusTelemetryMiddleware
6
+ from nexus.db.base import Base, NexusEntity
7
+
8
+ __all__ = [
9
+ "Base",
10
+ "GeminiClient",
11
+ "NexusEntity",
12
+ "NexusTelemetryMiddleware",
13
+ "create_nexus_app",
14
+ ]