vendor: update cxos-vendor-cargo
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"git": {
|
||||
"sha1": "d878deb8441897ecdd416011b49d2d2f6112e867"
|
||||
},
|
||||
"path_in_vcs": "crates/cxai-sdk"
|
||||
}
|
||||
Generated
+1926
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,86 @@
|
||||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# If you are reading this file be aware that the original Cargo.toml
|
||||
# will likely look very different (and much more reasonable).
|
||||
# See Cargo.toml.orig for the original contents.
|
||||
|
||||
[package]
|
||||
edition = "2024"
|
||||
name = "cxai-sdk"
|
||||
version = "0.1.0"
|
||||
authors = ["CxAI-LLM <agent@cxai-studio.com>"]
|
||||
build = false
|
||||
autolib = false
|
||||
autobins = false
|
||||
autoexamples = false
|
||||
autotests = false
|
||||
autobenches = false
|
||||
description = "Core SDK for the CxAI Platform — typed HTTP clients, models, resilience, and configuration"
|
||||
homepage = "https://cxllm.io"
|
||||
readme = false
|
||||
license = "MIT"
|
||||
repository = "https://git.cxllm-studio.com/CxAI-LLM/CxAI.Rust"
|
||||
resolver = "2"
|
||||
|
||||
[lib]
|
||||
name = "cxai_sdk"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies.anyhow]
|
||||
version = "1"
|
||||
|
||||
[dependencies.async-trait]
|
||||
version = "0.1"
|
||||
|
||||
[dependencies.backon]
|
||||
version = "1"
|
||||
|
||||
[dependencies.chrono]
|
||||
version = "0.4"
|
||||
features = ["serde"]
|
||||
|
||||
[dependencies.reqwest]
|
||||
version = "0.12"
|
||||
features = [
|
||||
"json",
|
||||
"rustls-tls",
|
||||
"stream",
|
||||
]
|
||||
default-features = false
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1"
|
||||
features = ["derive"]
|
||||
|
||||
[dependencies.serde_json]
|
||||
version = "1"
|
||||
|
||||
[dependencies.thiserror]
|
||||
version = "2"
|
||||
|
||||
[dependencies.tokio]
|
||||
version = "1"
|
||||
features = ["full"]
|
||||
|
||||
[dependencies.tracing]
|
||||
version = "0.1"
|
||||
|
||||
[dependencies.uuid]
|
||||
version = "1"
|
||||
features = [
|
||||
"v4",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[dev-dependencies.tokio]
|
||||
version = "1"
|
||||
features = [
|
||||
"full",
|
||||
"test-util",
|
||||
"macros",
|
||||
]
|
||||
Generated
+25
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "cxai-sdk"
|
||||
description = "Core SDK for the CxAI Platform — typed HTTP clients, models, resilience, and configuration"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[dependencies]
|
||||
tokio = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
backon = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["test-util", "macros"] }
|
||||
@@ -0,0 +1,66 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::config::PlatformConfig;
|
||||
use crate::error::{CxError, CxResult};
|
||||
use crate::models::common::HealthResponse;
|
||||
|
||||
/// App service client — application gateway, auth, chat, inference.
|
||||
#[derive(Clone)]
|
||||
pub struct AppClient {
|
||||
http: reqwest::Client,
|
||||
config: Arc<PlatformConfig>,
|
||||
}
|
||||
|
||||
impl AppClient {
|
||||
pub fn new(http: reqwest::Client, config: Arc<PlatformConfig>) -> Self {
|
||||
Self { http, config }
|
||||
}
|
||||
|
||||
fn url(&self, path: &str) -> String {
|
||||
format!(
|
||||
"{}/api/app{}",
|
||||
self.config.service_url(self.config.app_port),
|
||||
path
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn health(&self) -> CxResult<HealthResponse> {
|
||||
let resp = self.http.get(self.url("/healthz")).send().await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
pub async fn chat(
|
||||
&self,
|
||||
message: &str,
|
||||
session_id: Option<&str>,
|
||||
) -> CxResult<serde_json::Value> {
|
||||
let mut body = serde_json::json!({ "message": message });
|
||||
if let Some(sid) = session_id {
|
||||
body["sessionId"] = serde_json::json!(sid);
|
||||
}
|
||||
let resp = self.http.post(self.url("/chat")).json(&body).send().await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
pub async fn infer(
|
||||
&self,
|
||||
request: &crate::models::inference::CxInferenceRequest,
|
||||
) -> CxResult<crate::models::inference::InferenceResponse> {
|
||||
let resp = self
|
||||
.http
|
||||
.post(self.url("/infer"))
|
||||
.json(request)
|
||||
.send()
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::config::PlatformConfig;
|
||||
use crate::error::{CxError, CxResult};
|
||||
|
||||
/// Bridge service client — C++ IPC gateway, JSON-RPC, pub/sub.
|
||||
#[derive(Clone)]
|
||||
pub struct BridgeClient {
|
||||
http: reqwest::Client,
|
||||
config: Arc<PlatformConfig>,
|
||||
}
|
||||
|
||||
impl BridgeClient {
|
||||
pub fn new(http: reqwest::Client, config: Arc<PlatformConfig>) -> Self {
|
||||
Self { http, config }
|
||||
}
|
||||
|
||||
fn url(&self, path: &str) -> String {
|
||||
format!(
|
||||
"{}/api/bridge{}",
|
||||
self.config.service_url(self.config.bridge_port),
|
||||
path
|
||||
)
|
||||
}
|
||||
|
||||
/// Invoke an RPC method on the C++ backend.
|
||||
pub async fn rpc(
|
||||
&self,
|
||||
method: &str,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> CxResult<serde_json::Value> {
|
||||
let body = serde_json::json!({
|
||||
"method": method,
|
||||
"params": params,
|
||||
});
|
||||
let resp = self.http.post(self.url("/rpc")).json(&body).send().await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
/// Publish a message to a topic.
|
||||
pub async fn publish(&self, topic: &str, message: serde_json::Value) -> CxResult<()> {
|
||||
let body = serde_json::json!({
|
||||
"topic": topic,
|
||||
"message": message,
|
||||
});
|
||||
let resp = self
|
||||
.http
|
||||
.post(self.url("/publish"))
|
||||
.json(&body)
|
||||
.send()
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Discover available C++ modules and methods.
|
||||
pub async fn discover(&self) -> CxResult<serde_json::Value> {
|
||||
let resp = self.http.get(self.url("/discover")).send().await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
/// Get bridge connection status.
|
||||
pub async fn status(&self) -> CxResult<serde_json::Value> {
|
||||
let resp = self.http.get(self.url("/status")).send().await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
/// Get RPC call metrics.
|
||||
pub async fn metrics(&self) -> CxResult<serde_json::Value> {
|
||||
let resp = self.http.get(self.url("/metrics")).send().await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::config::PlatformConfig;
|
||||
use crate::error::{CxError, CxResult};
|
||||
|
||||
/// Codegen service client — MCP server, code intelligence.
|
||||
#[derive(Clone)]
|
||||
pub struct CodegenClient {
|
||||
http: reqwest::Client,
|
||||
config: Arc<PlatformConfig>,
|
||||
}
|
||||
|
||||
impl CodegenClient {
|
||||
pub fn new(http: reqwest::Client, config: Arc<PlatformConfig>) -> Self {
|
||||
Self { http, config }
|
||||
}
|
||||
|
||||
fn url(&self, path: &str) -> String {
|
||||
format!(
|
||||
"{}/api/codegen{}",
|
||||
self.config.service_url(self.config.codegen_port),
|
||||
path
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn analyze(&self, code: &str, language: &str) -> CxResult<serde_json::Value> {
|
||||
let body = serde_json::json!({
|
||||
"code": code,
|
||||
"language": language,
|
||||
});
|
||||
let resp = self
|
||||
.http
|
||||
.post(self.url("/analyze"))
|
||||
.json(&body)
|
||||
.send()
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
pub async fn generate(&self, prompt: &str, language: &str) -> CxResult<serde_json::Value> {
|
||||
let body = serde_json::json!({
|
||||
"prompt": prompt,
|
||||
"language": language,
|
||||
});
|
||||
let resp = self
|
||||
.http
|
||||
.post(self.url("/generate"))
|
||||
.json(&body)
|
||||
.send()
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
pub async fn list_resources(&self) -> CxResult<serde_json::Value> {
|
||||
let resp = self.http.get(self.url("/resources")).send().await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::config::PlatformConfig;
|
||||
use crate::error::{CxError, CxResult};
|
||||
use crate::models::commissioning::*;
|
||||
|
||||
/// Commissioning service client — BESS lifecycle management.
|
||||
#[derive(Clone)]
|
||||
pub struct CommissioningClient {
|
||||
http: reqwest::Client,
|
||||
config: Arc<PlatformConfig>,
|
||||
}
|
||||
|
||||
impl CommissioningClient {
|
||||
pub fn new(http: reqwest::Client, config: Arc<PlatformConfig>) -> Self {
|
||||
Self { http, config }
|
||||
}
|
||||
|
||||
fn url(&self, path: &str) -> String {
|
||||
format!(
|
||||
"{}/api/commissioning{}",
|
||||
self.config.service_url(self.config.commissioning_port),
|
||||
path
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn list_projects(&self) -> CxResult<Vec<CommissioningProject>> {
|
||||
let resp = self.http.get(self.url("/projects")).send().await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
pub async fn get_project(&self, id: &str) -> CxResult<CommissioningProject> {
|
||||
let resp = self
|
||||
.http
|
||||
.get(self.url(&format!("/projects/{id}")))
|
||||
.send()
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
pub async fn list_steps(&self, project_id: &str) -> CxResult<Vec<WorkflowStep>> {
|
||||
let resp = self
|
||||
.http
|
||||
.get(self.url(&format!("/projects/{project_id}/steps")))
|
||||
.send()
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
pub async fn list_equipment(&self, project_id: &str) -> CxResult<Vec<Equipment>> {
|
||||
let resp = self
|
||||
.http
|
||||
.get(self.url(&format!("/projects/{project_id}/equipment")))
|
||||
.send()
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
pub async fn list_incidents(&self, project_id: &str) -> CxResult<Vec<Incident>> {
|
||||
let resp = self
|
||||
.http
|
||||
.get(self.url(&format!("/projects/{project_id}/incidents")))
|
||||
.send()
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
pub async fn list_compliance(&self, project_id: &str) -> CxResult<Vec<ComplianceRecord>> {
|
||||
let resp = self
|
||||
.http
|
||||
.get(self.url(&format!("/projects/{project_id}/compliance")))
|
||||
.send()
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::config::PlatformConfig;
|
||||
use crate::error::{CxError, CxResult};
|
||||
|
||||
/// CxDocs service client — document engine.
|
||||
#[derive(Clone)]
|
||||
pub struct CxDocsClient {
|
||||
http: reqwest::Client,
|
||||
config: Arc<PlatformConfig>,
|
||||
}
|
||||
|
||||
impl CxDocsClient {
|
||||
pub fn new(http: reqwest::Client, config: Arc<PlatformConfig>) -> Self {
|
||||
Self { http, config }
|
||||
}
|
||||
|
||||
fn url(&self, path: &str) -> String {
|
||||
format!(
|
||||
"{}/api/cxdocs{}",
|
||||
self.config.service_url(self.config.cxdocs_port),
|
||||
path
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn list_documents(&self) -> CxResult<serde_json::Value> {
|
||||
let resp = self.http.get(self.url("/documents")).send().await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
pub async fn get_document(&self, id: &str) -> CxResult<serde_json::Value> {
|
||||
let resp = self
|
||||
.http
|
||||
.get(self.url(&format!("/documents/{id}")))
|
||||
.send()
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
pub async fn generate(&self, template: &str, data: serde_json::Value) -> CxResult<Vec<u8>> {
|
||||
let body = serde_json::json!({
|
||||
"template": template,
|
||||
"data": data,
|
||||
});
|
||||
let resp = self
|
||||
.http
|
||||
.post(self.url("/generate"))
|
||||
.json(&body)
|
||||
.send()
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.bytes().await?.to_vec())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::config::PlatformConfig;
|
||||
use crate::error::{CxError, CxResult};
|
||||
use crate::models::energy::*;
|
||||
|
||||
/// Energy service client — ERCOT market data, SCADA, ML predictions.
|
||||
#[derive(Clone)]
|
||||
pub struct EnergyClient {
|
||||
http: reqwest::Client,
|
||||
config: Arc<PlatformConfig>,
|
||||
}
|
||||
|
||||
impl EnergyClient {
|
||||
pub fn new(http: reqwest::Client, config: Arc<PlatformConfig>) -> Self {
|
||||
Self { http, config }
|
||||
}
|
||||
|
||||
fn url(&self, path: &str) -> String {
|
||||
format!(
|
||||
"{}/api/energy{}",
|
||||
self.config.service_url(self.config.energy_port),
|
||||
path
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn dam_prices(&self, settlement_point: &str) -> CxResult<Vec<DamPrice>> {
|
||||
let resp = self
|
||||
.http
|
||||
.get(self.url("/dam/prices"))
|
||||
.query(&[("settlementPoint", settlement_point)])
|
||||
.send()
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
pub async fn rt_prices(&self, settlement_point: &str) -> CxResult<Vec<RtPrice>> {
|
||||
let resp = self
|
||||
.http
|
||||
.get(self.url("/rt/prices"))
|
||||
.query(&[("settlementPoint", settlement_point)])
|
||||
.send()
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
pub async fn system_demand(&self) -> CxResult<SystemDemand> {
|
||||
let resp = self.http.get(self.url("/demand")).send().await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
pub async fn wind_generation(&self) -> CxResult<WindGeneration> {
|
||||
let resp = self.http.get(self.url("/wind")).send().await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
pub async fn solar_generation(&self) -> CxResult<SolarGeneration> {
|
||||
let resp = self.http.get(self.url("/solar")).send().await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
pub async fn market_summary(&self) -> CxResult<MarketSummary> {
|
||||
let resp = self.http.get(self.url("/summary")).send().await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
pub async fn scada_snapshot(&self, device_id: &str) -> CxResult<ScadaSnapshot> {
|
||||
let resp = self
|
||||
.http
|
||||
.get(self.url(&format!("/scada/{device_id}")))
|
||||
.send()
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
pub async fn predict(
|
||||
&self,
|
||||
prediction_type: &str,
|
||||
model: &str,
|
||||
target_date: &str,
|
||||
target_hour: u8,
|
||||
) -> CxResult<ErcotMlPrediction> {
|
||||
let body = serde_json::json!({
|
||||
"predictionType": prediction_type,
|
||||
"model": model,
|
||||
"targetDate": target_date,
|
||||
"targetHour": target_hour,
|
||||
});
|
||||
let resp = self
|
||||
.http
|
||||
.post(self.url("/predict"))
|
||||
.json(&body)
|
||||
.send()
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::config::PlatformConfig;
|
||||
use crate::error::{CxError, CxResult};
|
||||
use crate::models::forma::*;
|
||||
|
||||
/// Forma service client — Autodesk Forma/ACC integration.
|
||||
#[derive(Clone)]
|
||||
pub struct FormaClient {
|
||||
http: reqwest::Client,
|
||||
config: Arc<PlatformConfig>,
|
||||
}
|
||||
|
||||
impl FormaClient {
|
||||
pub fn new(http: reqwest::Client, config: Arc<PlatformConfig>) -> Self {
|
||||
Self { http, config }
|
||||
}
|
||||
|
||||
fn url(&self, path: &str) -> String {
|
||||
format!(
|
||||
"{}/api/forma{}",
|
||||
self.config.service_url(self.config.forma_port),
|
||||
path
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn list_projects(&self, token: &str) -> CxResult<Vec<FormaProjectInfo>> {
|
||||
let resp = self
|
||||
.http
|
||||
.get(self.url("/projects"))
|
||||
.bearer_auth(token)
|
||||
.send()
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
pub async fn list_elements(
|
||||
&self,
|
||||
token: &str,
|
||||
project_id: &str,
|
||||
) -> CxResult<Vec<FormaElementInfo>> {
|
||||
let resp = self
|
||||
.http
|
||||
.get(self.url(&format!("/projects/{project_id}/elements")))
|
||||
.bearer_auth(token)
|
||||
.send()
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
pub async fn trigger_analysis(
|
||||
&self,
|
||||
token: &str,
|
||||
project_id: &str,
|
||||
analysis_type: &str,
|
||||
parameters: serde_json::Value,
|
||||
) -> CxResult<FormaAnalysisResult> {
|
||||
let body = serde_json::json!({
|
||||
"analysisType": analysis_type,
|
||||
"parameters": parameters,
|
||||
});
|
||||
let resp = self
|
||||
.http
|
||||
.post(self.url(&format!("/projects/{project_id}/analysis")))
|
||||
.bearer_auth(token)
|
||||
.json(&body)
|
||||
.send()
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
pub async fn check_compliance(
|
||||
&self,
|
||||
token: &str,
|
||||
element_urn: &str,
|
||||
) -> CxResult<FormaComplianceResult> {
|
||||
let resp = self
|
||||
.http
|
||||
.get(self.url(&format!("/compliance/{element_urn}")))
|
||||
.bearer_auth(token)
|
||||
.send()
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
pub async fn list_issues(&self, token: &str, container_id: &str) -> CxResult<Vec<AccIssue>> {
|
||||
let resp = self
|
||||
.http
|
||||
.get(self.url(&format!("/acc/issues/{container_id}")))
|
||||
.bearer_auth(token)
|
||||
.send()
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
pub async fn list_checklists(
|
||||
&self,
|
||||
token: &str,
|
||||
project_id: &str,
|
||||
) -> CxResult<Vec<AccChecklist>> {
|
||||
let resp = self
|
||||
.http
|
||||
.get(self.url(&format!("/acc/checklists/{project_id}")))
|
||||
.bearer_auth(token)
|
||||
.send()
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::config::PlatformConfig;
|
||||
use crate::error::{CxError, CxResult};
|
||||
|
||||
/// ML service client — model catalog, prediction, anomaly detection.
|
||||
#[derive(Clone)]
|
||||
pub struct MlClient {
|
||||
http: reqwest::Client,
|
||||
config: Arc<PlatformConfig>,
|
||||
}
|
||||
|
||||
impl MlClient {
|
||||
pub fn new(http: reqwest::Client, config: Arc<PlatformConfig>) -> Self {
|
||||
Self { http, config }
|
||||
}
|
||||
|
||||
fn url(&self, path: &str) -> String {
|
||||
format!(
|
||||
"{}/api/ml{}",
|
||||
self.config.service_url(self.config.ml_port),
|
||||
path
|
||||
)
|
||||
}
|
||||
|
||||
/// Single model prediction.
|
||||
pub async fn predict(
|
||||
&self,
|
||||
model: &str,
|
||||
features: serde_json::Value,
|
||||
) -> CxResult<serde_json::Value> {
|
||||
let body = serde_json::json!({
|
||||
"model": model,
|
||||
"features": features,
|
||||
});
|
||||
let resp = self
|
||||
.http
|
||||
.post(self.url("/predict"))
|
||||
.json(&body)
|
||||
.send()
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
/// Batch prediction across multiple models.
|
||||
pub async fn predict_batch(
|
||||
&self,
|
||||
requests: Vec<serde_json::Value>,
|
||||
) -> CxResult<serde_json::Value> {
|
||||
let resp = self
|
||||
.http
|
||||
.post(self.url("/predict/batch"))
|
||||
.json(&requests)
|
||||
.send()
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
/// Anomaly detection using IsolationForest.
|
||||
pub async fn detect_anomalies(
|
||||
&self,
|
||||
features: serde_json::Value,
|
||||
) -> CxResult<serde_json::Value> {
|
||||
let body = serde_json::json!({ "features": features });
|
||||
let resp = self
|
||||
.http
|
||||
.post(self.url("/detect-anomalies"))
|
||||
.json(&body)
|
||||
.send()
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
/// Classify price regime (4-class).
|
||||
pub async fn classify_regime(
|
||||
&self,
|
||||
features: serde_json::Value,
|
||||
) -> CxResult<serde_json::Value> {
|
||||
let body = serde_json::json!({ "features": features });
|
||||
let resp = self
|
||||
.http
|
||||
.post(self.url("/classify-regime"))
|
||||
.json(&body)
|
||||
.send()
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
/// List all 26 models in the catalog.
|
||||
pub async fn list_models(&self) -> CxResult<serde_json::Value> {
|
||||
let resp = self.http.get(self.url("/models")).send().await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
/// List model domains.
|
||||
pub async fn list_domains(&self) -> CxResult<serde_json::Value> {
|
||||
let resp = self.http.get(self.url("/domains")).send().await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
/// ERCOT price forecast.
|
||||
pub async fn price_forecast(
|
||||
&self,
|
||||
settlement_point: &str,
|
||||
hours_ahead: u32,
|
||||
) -> CxResult<serde_json::Value> {
|
||||
let body = serde_json::json!({
|
||||
"settlementPoint": settlement_point,
|
||||
"hoursAhead": hours_ahead,
|
||||
});
|
||||
let resp = self
|
||||
.http
|
||||
.post(self.url("/intelligence/price-forecast"))
|
||||
.json(&body)
|
||||
.send()
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
/// Backend health check.
|
||||
pub async fn backend_health(&self) -> CxResult<serde_json::Value> {
|
||||
let resp = self.http.get(self.url("/backend-health")).send().await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
mod app;
|
||||
mod bridge;
|
||||
mod codegen;
|
||||
mod commissioning;
|
||||
mod cxdocs;
|
||||
mod energy;
|
||||
mod forma;
|
||||
mod ml;
|
||||
mod nemo;
|
||||
mod nvidia;
|
||||
mod studio;
|
||||
mod supabase;
|
||||
|
||||
pub use app::AppClient;
|
||||
pub use bridge::BridgeClient;
|
||||
pub use codegen::CodegenClient;
|
||||
pub use commissioning::CommissioningClient;
|
||||
pub use cxdocs::CxDocsClient;
|
||||
pub use energy::EnergyClient;
|
||||
pub use forma::FormaClient;
|
||||
pub use ml::MlClient;
|
||||
pub use nemo::NemoClient;
|
||||
pub use nvidia::NvidiaClient;
|
||||
pub use studio::StudioClient;
|
||||
pub use supabase::SupabaseClient;
|
||||
@@ -0,0 +1,126 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::config::PlatformConfig;
|
||||
use crate::error::{CxError, CxResult};
|
||||
use crate::models::inference::*;
|
||||
use crate::models::nemo::*;
|
||||
|
||||
/// Nemo service client — AI agent orchestration.
|
||||
#[derive(Clone)]
|
||||
pub struct NemoClient {
|
||||
http: reqwest::Client,
|
||||
config: Arc<PlatformConfig>,
|
||||
}
|
||||
|
||||
impl NemoClient {
|
||||
pub fn new(http: reqwest::Client, config: Arc<PlatformConfig>) -> Self {
|
||||
Self { http, config }
|
||||
}
|
||||
|
||||
fn url(&self, path: &str) -> String {
|
||||
format!(
|
||||
"{}/api/nemo{}",
|
||||
self.config.service_url(self.config.nemo_port),
|
||||
path
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn run_agent(&self, request: &AgentRunRequest) -> CxResult<AgentRunResponse> {
|
||||
let resp = self
|
||||
.http
|
||||
.post(self.url("/agent/run"))
|
||||
.json(request)
|
||||
.send()
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
pub async fn chat(&self, request: &NemoChatRequest) -> CxResult<NemoChatResponse> {
|
||||
let resp = self
|
||||
.http
|
||||
.post(self.url("/chat"))
|
||||
.json(request)
|
||||
.send()
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
pub async fn list_sessions(&self) -> CxResult<Vec<NemoSessionSummary>> {
|
||||
let resp = self.http.get(self.url("/sessions")).send().await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
pub async fn get_session(&self, session_id: &str) -> CxResult<NemoSession> {
|
||||
let resp = self
|
||||
.http
|
||||
.get(self.url(&format!("/sessions/{session_id}")))
|
||||
.send()
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
pub async fn list_tools(&self) -> CxResult<Vec<ToolDefinition>> {
|
||||
let resp = self.http.get(self.url("/tools")).send().await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
pub async fn execute_tool(
|
||||
&self,
|
||||
request: &ToolExecutionRequest,
|
||||
) -> CxResult<ToolExecutionResult> {
|
||||
let resp = self
|
||||
.http
|
||||
.post(self.url("/tools/execute"))
|
||||
.json(request)
|
||||
.send()
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
pub async fn list_profiles(&self) -> CxResult<Vec<InferenceProfile>> {
|
||||
let resp = self.http.get(self.url("/profiles")).send().await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
pub async fn route(&self, request: &RouteRequest) -> CxResult<RouteResponse> {
|
||||
let resp = self
|
||||
.http
|
||||
.post(self.url("/route"))
|
||||
.json(request)
|
||||
.send()
|
||||
.await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
pub async fn metrics(&self) -> CxResult<NemoMetrics> {
|
||||
let resp = self.http.get(self.url("/metrics")).send().await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::config::PlatformConfig;
|
||||
use crate::error::{CxError, CxResult};
|
||||
use crate::models::ChatMessage;
|
||||
use crate::models::inference::*;
|
||||
|
||||
/// NVIDIA NIM inference client — chat, embedding, reranking, vision, guardrails.
|
||||
#[derive(Clone)]
|
||||
pub struct NvidiaClient {
|
||||
http: reqwest::Client,
|
||||
config: Arc<PlatformConfig>,
|
||||
}
|
||||
|
||||
impl NvidiaClient {
|
||||
pub fn new(http: reqwest::Client, config: Arc<PlatformConfig>) -> Self {
|
||||
Self { http, config }
|
||||
}
|
||||
|
||||
#[allow(clippy::misnamed_getters)]
|
||||
fn base_url(&self) -> &str {
|
||||
&self.config.nvidia_base_url
|
||||
}
|
||||
|
||||
fn api_key(&self) -> CxResult<&str> {
|
||||
self.config
|
||||
.nvidia_api_key
|
||||
.as_deref()
|
||||
.ok_or_else(|| CxError::Config("NVIDIA_API_KEY not set".into()))
|
||||
}
|
||||
|
||||
/// Run chat completion.
|
||||
pub async fn chat(&self, request: &InferenceRequest) -> CxResult<InferenceResponse> {
|
||||
let resp = self
|
||||
.http
|
||||
.post(format!("{}/chat/completions", self.base_url()))
|
||||
.bearer_auth(self.api_key()?)
|
||||
.json(request)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
/// Generate embeddings for text.
|
||||
pub async fn embed(&self, text: &str) -> CxResult<Vec<f32>> {
|
||||
let body = serde_json::json!({
|
||||
"input": text,
|
||||
"model": "nvidia/nv-embedqa-e5-v5"
|
||||
});
|
||||
|
||||
let resp = self
|
||||
.http
|
||||
.post(format!("{}/embeddings", self.base_url()))
|
||||
.bearer_auth(self.api_key()?)
|
||||
.json(&body)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
|
||||
let data: serde_json::Value = resp.json().await?;
|
||||
let embedding = data["data"][0]["embedding"]
|
||||
.as_array()
|
||||
.ok_or_else(|| CxError::Api {
|
||||
status: 500,
|
||||
message: "missing embedding data".into(),
|
||||
})?
|
||||
.iter()
|
||||
.filter_map(|v| v.as_f64().map(|f| f as f32))
|
||||
.collect();
|
||||
|
||||
Ok(embedding)
|
||||
}
|
||||
|
||||
/// Rerank documents against a query.
|
||||
pub async fn rerank(&self, query: &str, documents: &[String]) -> CxResult<InferenceResponse> {
|
||||
let body = serde_json::json!({
|
||||
"model": "nvidia/nv-rerankqa-mistral-4b-v3",
|
||||
"query": { "text": query },
|
||||
"passages": documents.iter().map(|d| serde_json::json!({"text": d})).collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
let resp = self
|
||||
.http
|
||||
.post(format!("{}/ranking", self.base_url()))
|
||||
.bearer_auth(self.api_key()?)
|
||||
.json(&body)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
/// Vision analysis on an image URL.
|
||||
pub async fn vision(&self, prompt: &str, image_url: &str) -> CxResult<InferenceResponse> {
|
||||
let request = InferenceRequest {
|
||||
model: "meta/llama-4-maverick-17b-128e-instruct".into(),
|
||||
messages: vec![ChatMessage {
|
||||
role: "user".into(),
|
||||
content: format!(
|
||||
r#"[{{"type":"text","text":"{prompt}"}},{{"type":"image_url","image_url":{{"url":"{image_url}"}}}}]"#
|
||||
),
|
||||
timestamp: None,
|
||||
}],
|
||||
temperature: Some(0.2),
|
||||
max_tokens: Some(1024),
|
||||
stream: Some(false),
|
||||
top_p: None,
|
||||
};
|
||||
|
||||
self.chat(&request).await
|
||||
}
|
||||
|
||||
/// Run guardrail safety check.
|
||||
pub async fn guardrail(&self, text: &str) -> CxResult<GuardrailResult> {
|
||||
let body = serde_json::json!({
|
||||
"model": "nvidia/llama-3.1-nemoguard-8b-content-safety",
|
||||
"messages": [{"role": "user", "content": text}]
|
||||
});
|
||||
|
||||
let resp = self
|
||||
.http
|
||||
.post(format!("{}/chat/completions", self.base_url()))
|
||||
.bearer_auth(self.api_key()?)
|
||||
.json(&body)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::config::PlatformConfig;
|
||||
use crate::error::{CxError, CxResult};
|
||||
use crate::models::common::*;
|
||||
use crate::models::studio::*;
|
||||
|
||||
/// Studio service client — platform hub, service registry, deployments.
|
||||
#[derive(Clone)]
|
||||
pub struct StudioClient {
|
||||
http: reqwest::Client,
|
||||
config: Arc<PlatformConfig>,
|
||||
}
|
||||
|
||||
impl StudioClient {
|
||||
pub fn new(http: reqwest::Client, config: Arc<PlatformConfig>) -> Self {
|
||||
Self { http, config }
|
||||
}
|
||||
|
||||
fn url(&self, path: &str) -> String {
|
||||
format!(
|
||||
"{}/api/studio{}",
|
||||
self.config.service_url(self.config.studio_port),
|
||||
path
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn health(&self) -> CxResult<HealthResponse> {
|
||||
let resp = self.http.get(self.url("/healthz")).send().await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
pub async fn info(&self) -> CxResult<PlatformInfo> {
|
||||
let resp = self.http.get(self.url("/info")).send().await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
pub async fn list_services(&self) -> CxResult<Vec<Service>> {
|
||||
let resp = self.http.get(self.url("/services")).send().await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
pub async fn list_deployments(&self) -> CxResult<Vec<Deployment>> {
|
||||
let resp = self.http.get(self.url("/deployments")).send().await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
pub async fn list_conversations(&self) -> CxResult<Vec<Conversation>> {
|
||||
let resp = self.http.get(self.url("/conversations")).send().await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::config::PlatformConfig;
|
||||
use crate::error::{CxError, CxResult};
|
||||
|
||||
/// Supabase client — PostgreSQL queries and Storage operations.
|
||||
#[derive(Clone)]
|
||||
pub struct SupabaseClient {
|
||||
http: reqwest::Client,
|
||||
config: Arc<PlatformConfig>,
|
||||
}
|
||||
|
||||
impl SupabaseClient {
|
||||
pub fn new(http: reqwest::Client, config: Arc<PlatformConfig>) -> Self {
|
||||
Self { http, config }
|
||||
}
|
||||
|
||||
fn base_url(&self) -> CxResult<&str> {
|
||||
self.config
|
||||
.supabase_url
|
||||
.as_deref()
|
||||
.ok_or_else(|| CxError::Config("SUPABASE_URL not set".into()))
|
||||
}
|
||||
|
||||
fn service_key(&self) -> CxResult<&str> {
|
||||
self.config
|
||||
.supabase_service_role_key
|
||||
.as_deref()
|
||||
.ok_or_else(|| CxError::Config("SUPABASE_SERVICE_ROLE_KEY not set".into()))
|
||||
}
|
||||
|
||||
/// Query rows from a table.
|
||||
pub async fn query<T: serde::de::DeserializeOwned>(
|
||||
&self,
|
||||
table: &str,
|
||||
select: Option<&str>,
|
||||
filter: Option<&str>,
|
||||
) -> CxResult<Vec<T>> {
|
||||
let mut url = format!("{}/rest/v1/{}", self.base_url()?, table);
|
||||
let mut params = vec![];
|
||||
if let Some(s) = select {
|
||||
params.push(format!("select={s}"));
|
||||
}
|
||||
if let Some(f) = filter {
|
||||
params.push(f.to_string());
|
||||
}
|
||||
if !params.is_empty() {
|
||||
url = format!("{}?{}", url, params.join("&"));
|
||||
}
|
||||
|
||||
let resp = self
|
||||
.http
|
||||
.get(&url)
|
||||
.header("apikey", self.service_key()?)
|
||||
.header("Authorization", format!("Bearer {}", self.service_key()?))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
/// Insert a row.
|
||||
pub async fn insert<T: serde::Serialize + serde::de::DeserializeOwned>(
|
||||
&self,
|
||||
table: &str,
|
||||
data: &T,
|
||||
) -> CxResult<T> {
|
||||
let url = format!("{}/rest/v1/{}", self.base_url()?, table);
|
||||
let resp = self
|
||||
.http
|
||||
.post(&url)
|
||||
.header("apikey", self.service_key()?)
|
||||
.header("Authorization", format!("Bearer {}", self.service_key()?))
|
||||
.header("Prefer", "return=representation")
|
||||
.json(data)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
|
||||
let mut items: Vec<T> = resp.json().await?;
|
||||
items.pop().ok_or_else(|| CxError::Api {
|
||||
status: 500,
|
||||
message: "empty insert response".into(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Delete rows matching a filter.
|
||||
pub async fn delete(&self, table: &str, filter: &str) -> CxResult<()> {
|
||||
let url = format!("{}/rest/v1/{}?{}", self.base_url()?, table, filter);
|
||||
let resp = self
|
||||
.http
|
||||
.delete(&url)
|
||||
.header("apikey", self.service_key()?)
|
||||
.header("Authorization", format!("Bearer {}", self.service_key()?))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Upload a file to Supabase Storage.
|
||||
pub async fn upload_file(
|
||||
&self,
|
||||
bucket: &str,
|
||||
path: &str,
|
||||
data: Vec<u8>,
|
||||
content_type: &str,
|
||||
) -> CxResult<()> {
|
||||
let url = format!("{}/storage/v1/object/{}/{}", self.base_url()?, bucket, path);
|
||||
let resp = self
|
||||
.http
|
||||
.post(&url)
|
||||
.header("apikey", self.service_key()?)
|
||||
.header("Authorization", format!("Bearer {}", self.service_key()?))
|
||||
.header("Content-Type", content_type)
|
||||
.body(data)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a signed download URL.
|
||||
pub async fn signed_url(&self, bucket: &str, path: &str, expires_in: u64) -> CxResult<String> {
|
||||
let url = format!(
|
||||
"{}/storage/v1/object/sign/{}/{}",
|
||||
self.base_url()?,
|
||||
bucket,
|
||||
path
|
||||
);
|
||||
let body = serde_json::json!({ "expiresIn": expires_in });
|
||||
let resp = self
|
||||
.http
|
||||
.post(&url)
|
||||
.header("apikey", self.service_key()?)
|
||||
.header("Authorization", format!("Bearer {}", self.service_key()?))
|
||||
.json(&body)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
return Err(CxError::from_response(resp).await);
|
||||
}
|
||||
|
||||
let data: serde_json::Value = resp.json().await?;
|
||||
data["signedURL"]
|
||||
.as_str()
|
||||
.map(|s| s.to_string())
|
||||
.ok_or_else(|| CxError::Api {
|
||||
status: 500,
|
||||
message: "missing signedURL".into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
use crate::error::{CxError, CxResult};
|
||||
|
||||
/// Platform-wide configuration.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlatformConfig {
|
||||
pub base_url: String,
|
||||
pub api_token: Option<String>,
|
||||
pub timeout_secs: u64,
|
||||
pub pool_size: usize,
|
||||
|
||||
// Service ports
|
||||
pub studio_port: u16,
|
||||
pub commissioning_port: u16,
|
||||
pub energy_port: u16,
|
||||
pub forma_port: u16,
|
||||
pub cxdocs_port: u16,
|
||||
pub app_port: u16,
|
||||
pub nemo_port: u16,
|
||||
pub codegen_port: u16,
|
||||
pub bridge_port: u16,
|
||||
pub ml_port: u16,
|
||||
|
||||
// NVIDIA
|
||||
pub nvidia_api_key: Option<String>,
|
||||
pub nvidia_base_url: String,
|
||||
|
||||
// Supabase
|
||||
pub supabase_url: Option<String>,
|
||||
pub supabase_service_role_key: Option<String>,
|
||||
|
||||
// ERCOT
|
||||
pub ercot_username: Option<String>,
|
||||
pub ercot_password: Option<String>,
|
||||
pub ercot_subscription_key: Option<String>,
|
||||
|
||||
// MongoDB
|
||||
pub mongodb_uri: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for PlatformConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
base_url: "http://localhost".into(),
|
||||
api_token: None,
|
||||
timeout_secs: 30,
|
||||
pool_size: 10,
|
||||
studio_port: 8080,
|
||||
commissioning_port: 8081,
|
||||
energy_port: 8082,
|
||||
forma_port: 8083,
|
||||
cxdocs_port: 8084,
|
||||
app_port: 8090,
|
||||
nemo_port: 18800,
|
||||
codegen_port: 18900,
|
||||
bridge_port: 9100,
|
||||
ml_port: 8085,
|
||||
nvidia_api_key: None,
|
||||
nvidia_base_url: "https://integrate.api.nvidia.com/v1".into(),
|
||||
supabase_url: None,
|
||||
supabase_service_role_key: None,
|
||||
ercot_username: None,
|
||||
ercot_password: None,
|
||||
ercot_subscription_key: None,
|
||||
mongodb_uri: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformConfig {
|
||||
/// Load configuration from environment variables.
|
||||
pub fn from_env() -> CxResult<Self> {
|
||||
let env = |key: &str| std::env::var(key).ok();
|
||||
|
||||
Ok(Self {
|
||||
base_url: env("CXAI_BASE_URL").unwrap_or_else(|| "http://localhost".into()),
|
||||
api_token: env("CXAI_API_TOKEN"),
|
||||
timeout_secs: env("CXAI_TIMEOUT_SECS")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(30),
|
||||
pool_size: env("CXAI_POOL_SIZE")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(10),
|
||||
studio_port: parse_port("CXAI_STUDIO_PORT", 8080)?,
|
||||
commissioning_port: parse_port("CXAI_COMMISSIONING_PORT", 8081)?,
|
||||
energy_port: parse_port("CXAI_ENERGY_PORT", 8082)?,
|
||||
forma_port: parse_port("CXAI_FORMA_PORT", 8083)?,
|
||||
cxdocs_port: parse_port("CXAI_CXDOCS_PORT", 8084)?,
|
||||
app_port: parse_port("CXAI_APP_PORT", 8090)?,
|
||||
nemo_port: parse_port("CXAI_NEMO_PORT", 18800)?,
|
||||
codegen_port: parse_port("CXAI_CODEGEN_PORT", 18900)?,
|
||||
bridge_port: parse_port("CXAI_BRIDGE_PORT", 9100)?,
|
||||
ml_port: parse_port("CXAI_ML_PORT", 8085)?,
|
||||
nvidia_api_key: env("NVIDIA_API_KEY"),
|
||||
nvidia_base_url: env("NVIDIA_BASE_URL")
|
||||
.unwrap_or_else(|| "https://integrate.api.nvidia.com/v1".into()),
|
||||
supabase_url: env("SUPABASE_URL"),
|
||||
supabase_service_role_key: env("SUPABASE_SERVICE_ROLE_KEY"),
|
||||
ercot_username: env("ERCOT_USERNAME"),
|
||||
ercot_password: env("ERCOT_PASSWORD"),
|
||||
ercot_subscription_key: env("ERCOT_SUBSCRIPTION_KEY"),
|
||||
mongodb_uri: env("MONGODB_URI"),
|
||||
})
|
||||
}
|
||||
|
||||
/// Build the full URL for a service.
|
||||
pub fn service_url(&self, port: u16) -> String {
|
||||
format!("{}:{}", self.base_url, port)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_port(key: &str, default: u16) -> CxResult<u16> {
|
||||
match std::env::var(key) {
|
||||
Ok(v) => v
|
||||
.parse()
|
||||
.map_err(|_| CxError::Config(format!("{key}: invalid port '{v}'"))),
|
||||
Err(_) => Ok(default),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
use thiserror::Error;
|
||||
|
||||
/// Platform SDK error type.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum CxError {
|
||||
#[error("HTTP error: {0}")]
|
||||
Http(#[from] reqwest::Error),
|
||||
|
||||
#[error("API error ({status}): {message}")]
|
||||
Api { status: u16, message: String },
|
||||
|
||||
#[error("JSON error: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
|
||||
#[error("Configuration error: {0}")]
|
||||
Config(String),
|
||||
|
||||
#[error("Not found: {0}")]
|
||||
NotFound(String),
|
||||
|
||||
#[error("Unauthorized: {0}")]
|
||||
Unauthorized(String),
|
||||
|
||||
#[error("Timeout after {0}s")]
|
||||
Timeout(u64),
|
||||
|
||||
#[error("Circuit breaker open for {service}")]
|
||||
CircuitOpen { service: String },
|
||||
}
|
||||
|
||||
pub type CxResult<T> = Result<T, CxError>;
|
||||
|
||||
impl CxError {
|
||||
/// Create from an HTTP response with non-2xx status.
|
||||
pub async fn from_response(response: reqwest::Response) -> Self {
|
||||
let status = response.status().as_u16();
|
||||
let message = response
|
||||
.text()
|
||||
.await
|
||||
.unwrap_or_else(|_| "unknown error".into());
|
||||
|
||||
match status {
|
||||
401 | 403 => Self::Unauthorized(message),
|
||||
404 => Self::NotFound(message),
|
||||
_ => Self::Api { status, message },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
pub mod clients;
|
||||
pub mod config;
|
||||
pub mod error;
|
||||
pub mod models;
|
||||
pub mod resilience;
|
||||
|
||||
pub use config::PlatformConfig;
|
||||
pub use error::{CxError, CxResult};
|
||||
|
||||
use clients::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Central SDK handle providing access to all platform service clients.
|
||||
pub struct CxPlatform {
|
||||
config: Arc<PlatformConfig>,
|
||||
http: reqwest::Client,
|
||||
}
|
||||
|
||||
impl CxPlatform {
|
||||
/// Create a new platform SDK instance from configuration.
|
||||
pub fn new(config: PlatformConfig) -> CxResult<Self> {
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
if let Some(ref token) = config.api_token {
|
||||
headers.insert(
|
||||
reqwest::header::AUTHORIZATION,
|
||||
reqwest::header::HeaderValue::from_str(&format!("Bearer {token}"))
|
||||
.map_err(|e| CxError::Config(format!("invalid token: {e}")))?,
|
||||
);
|
||||
}
|
||||
|
||||
let http = reqwest::Client::builder()
|
||||
.default_headers(headers)
|
||||
.timeout(std::time::Duration::from_secs(config.timeout_secs))
|
||||
.pool_max_idle_per_host(config.pool_size)
|
||||
.build()
|
||||
.map_err(|e| CxError::Config(format!("http client: {e}")))?;
|
||||
|
||||
Ok(Self {
|
||||
config: Arc::new(config),
|
||||
http,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create from environment variables.
|
||||
pub fn from_env() -> CxResult<Self> {
|
||||
Self::new(PlatformConfig::from_env()?)
|
||||
}
|
||||
|
||||
pub fn nvidia(&self) -> NvidiaClient {
|
||||
NvidiaClient::new(self.http.clone(), self.config.clone())
|
||||
}
|
||||
|
||||
pub fn supabase(&self) -> SupabaseClient {
|
||||
SupabaseClient::new(self.http.clone(), self.config.clone())
|
||||
}
|
||||
|
||||
pub fn studio(&self) -> StudioClient {
|
||||
StudioClient::new(self.http.clone(), self.config.clone())
|
||||
}
|
||||
|
||||
pub fn commissioning(&self) -> CommissioningClient {
|
||||
CommissioningClient::new(self.http.clone(), self.config.clone())
|
||||
}
|
||||
|
||||
pub fn energy(&self) -> EnergyClient {
|
||||
EnergyClient::new(self.http.clone(), self.config.clone())
|
||||
}
|
||||
|
||||
pub fn forma(&self) -> FormaClient {
|
||||
FormaClient::new(self.http.clone(), self.config.clone())
|
||||
}
|
||||
|
||||
pub fn cxdocs(&self) -> CxDocsClient {
|
||||
CxDocsClient::new(self.http.clone(), self.config.clone())
|
||||
}
|
||||
|
||||
pub fn app(&self) -> AppClient {
|
||||
AppClient::new(self.http.clone(), self.config.clone())
|
||||
}
|
||||
|
||||
pub fn nemo(&self) -> NemoClient {
|
||||
NemoClient::new(self.http.clone(), self.config.clone())
|
||||
}
|
||||
|
||||
pub fn codegen(&self) -> CodegenClient {
|
||||
CodegenClient::new(self.http.clone(), self.config.clone())
|
||||
}
|
||||
|
||||
pub fn bridge(&self) -> BridgeClient {
|
||||
BridgeClient::new(self.http.clone(), self.config.clone())
|
||||
}
|
||||
|
||||
pub fn ml(&self) -> MlClient {
|
||||
MlClient::new(self.http.clone(), self.config.clone())
|
||||
}
|
||||
|
||||
pub fn config(&self) -> &PlatformConfig {
|
||||
&self.config
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn platform_from_default_config() {
|
||||
let config = PlatformConfig::default();
|
||||
let platform = CxPlatform::new(config).unwrap();
|
||||
assert_eq!(platform.config().base_url, "http://localhost");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn platform_clients_are_constructible() {
|
||||
let platform = CxPlatform::new(PlatformConfig::default()).unwrap();
|
||||
let _ = platform.nvidia();
|
||||
let _ = platform.studio();
|
||||
let _ = platform.commissioning();
|
||||
let _ = platform.energy();
|
||||
let _ = platform.forma();
|
||||
let _ = platform.cxdocs();
|
||||
let _ = platform.app();
|
||||
let _ = platform.nemo();
|
||||
let _ = platform.codegen();
|
||||
let _ = platform.bridge();
|
||||
let _ = platform.ml();
|
||||
let _ = platform.supabase();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CommissioningProject {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub phase: String,
|
||||
pub status: String,
|
||||
pub capacity_mw: Option<f64>,
|
||||
pub manager: Option<String>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WorkflowStep {
|
||||
pub id: String,
|
||||
pub project_id: String,
|
||||
pub name: String,
|
||||
pub status: String,
|
||||
pub order: u32,
|
||||
pub completed_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Equipment {
|
||||
pub id: String,
|
||||
pub project_id: String,
|
||||
pub name: String,
|
||||
pub equipment_type: String,
|
||||
pub serial_number: Option<String>,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Incident {
|
||||
pub id: String,
|
||||
pub project_id: String,
|
||||
pub title: String,
|
||||
pub severity: String,
|
||||
pub status: String,
|
||||
pub description: Option<String>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub resolved_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ComplianceRecord {
|
||||
pub id: String,
|
||||
pub project_id: String,
|
||||
pub standard: String,
|
||||
pub status: String,
|
||||
pub verified_at: Option<DateTime<Utc>>,
|
||||
pub notes: Option<String>,
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Generic API response wrapper matching .NET ApiResponse<T>.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ApiResponse<T> {
|
||||
pub success: bool,
|
||||
pub data: Option<T>,
|
||||
pub error: Option<String>,
|
||||
pub errors: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl<T> ApiResponse<T> {
|
||||
pub fn ok(data: T) -> Self {
|
||||
Self {
|
||||
success: true,
|
||||
data: Some(data),
|
||||
error: None,
|
||||
errors: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn err(message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
success: false,
|
||||
data: None,
|
||||
error: Some(message.into()),
|
||||
errors: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Paginated response matching .NET PaginatedResponse<T>.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PaginatedResponse<T> {
|
||||
pub items: Vec<T>,
|
||||
pub total: i64,
|
||||
pub page: i32,
|
||||
pub per_page: i32,
|
||||
}
|
||||
|
||||
/// Health status response from /healthz endpoints.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HealthResponse {
|
||||
pub status: String,
|
||||
pub version: String,
|
||||
pub environment: String,
|
||||
pub services: Option<HashMap<String, ServiceHealth>>,
|
||||
}
|
||||
|
||||
/// Individual service health.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ServiceHealth {
|
||||
pub status: String,
|
||||
pub latency_ms: Option<f64>,
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
/// Platform info from /info endpoint.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PlatformInfo {
|
||||
pub service: String,
|
||||
pub version: String,
|
||||
pub environment: String,
|
||||
pub uptime: String,
|
||||
pub runtime: String,
|
||||
pub host: String,
|
||||
}
|
||||
|
||||
/// Metrics from /metrics endpoint.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PlatformMetrics {
|
||||
pub uptime_seconds: f64,
|
||||
pub total_requests: u64,
|
||||
pub total_errors: u64,
|
||||
pub memory_mb: f64,
|
||||
pub threads: u32,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Chat message matching .NET ChatMessage.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ChatMessage {
|
||||
pub role: String,
|
||||
pub content: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub timestamp: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl ChatMessage {
|
||||
pub fn system(content: impl Into<String>) -> Self {
|
||||
Self {
|
||||
role: "system".into(),
|
||||
content: content.into(),
|
||||
timestamp: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn user(content: impl Into<String>) -> Self {
|
||||
Self {
|
||||
role: "user".into(),
|
||||
content: content.into(),
|
||||
timestamp: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn assistant(content: impl Into<String>) -> Self {
|
||||
Self {
|
||||
role: "assistant".into(),
|
||||
content: content.into(),
|
||||
timestamp: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
use chrono::{DateTime, NaiveDate, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DamPrice {
|
||||
pub settlement_point: String,
|
||||
pub delivery_date: NaiveDate,
|
||||
pub hour_ending: u8,
|
||||
pub price: f64,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub timestamp: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RtPrice {
|
||||
pub settlement_point: String,
|
||||
pub price: f64,
|
||||
pub interval: String,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SystemDemand {
|
||||
pub demand_mw: f64,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub forecast_mw: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WindGeneration {
|
||||
pub generation_mw: f64,
|
||||
pub capacity_mw: f64,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SolarGeneration {
|
||||
pub generation_mw: f64,
|
||||
pub capacity_mw: f64,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MarketSummary {
|
||||
pub avg_dam_price: f64,
|
||||
pub avg_rt_price: f64,
|
||||
pub peak_demand_mw: f64,
|
||||
pub wind_generation_mw: f64,
|
||||
pub solar_generation_mw: f64,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ErcotMlPrediction {
|
||||
pub prediction_type: String,
|
||||
pub model: String,
|
||||
pub target_date: NaiveDate,
|
||||
pub target_hour: u8,
|
||||
pub predicted_value: f64,
|
||||
pub confidence: f64,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub lower_bound: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub upper_bound: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ScadaSnapshot {
|
||||
pub device_id: String,
|
||||
pub soc_percent: f64,
|
||||
pub soh_percent: f64,
|
||||
pub power_kw: f64,
|
||||
pub voltage_v: f64,
|
||||
pub current_a: f64,
|
||||
pub temperature_c: f64,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CongestionSnapshot {
|
||||
pub constraint_name: String,
|
||||
pub shadow_price: f64,
|
||||
pub limit_mw: f64,
|
||||
pub flow_mw: f64,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PriceVolatility {
|
||||
pub settlement_point: String,
|
||||
pub volatility: f64,
|
||||
pub period_hours: u32,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FormaTokenResponse {
|
||||
pub access_token: String,
|
||||
pub token_type: String,
|
||||
pub expires_in: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FormaProjectInfo {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub status: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FormaElementInfo {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub element_type: String,
|
||||
pub project_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FormaAnalysisResult {
|
||||
pub id: String,
|
||||
pub analysis_type: String,
|
||||
pub status: String,
|
||||
pub result: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FormaComplianceResult {
|
||||
pub compliant: bool,
|
||||
pub violations: Vec<String>,
|
||||
pub element_urn: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AccFolder {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub parent_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AccIssue {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub status: String,
|
||||
pub priority: Option<String>,
|
||||
pub assigned_to: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AccChecklist {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub status: String,
|
||||
pub items_total: u32,
|
||||
pub items_completed: u32,
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Inference request matching .NET InferenceRequest.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InferenceRequest {
|
||||
pub model: String,
|
||||
pub messages: Vec<super::ChatMessage>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub temperature: Option<f32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub max_tokens: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub stream: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub top_p: Option<f32>,
|
||||
}
|
||||
|
||||
/// Inference response matching .NET InferenceResponse.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InferenceResponse {
|
||||
pub content: String,
|
||||
pub model: String,
|
||||
pub usage: Option<TokenUsage>,
|
||||
pub finish_reason: Option<String>,
|
||||
}
|
||||
|
||||
/// Token usage stats.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TokenUsage {
|
||||
pub prompt_tokens: u32,
|
||||
pub completion_tokens: u32,
|
||||
pub total_tokens: u32,
|
||||
}
|
||||
|
||||
/// CxModel record matching .NET CxModelRecord.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CxModelRecord {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub provider: String,
|
||||
pub model_id: String,
|
||||
pub status: String,
|
||||
pub capabilities: CxModelCapabilities,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub created_at: Option<DateTime<Utc>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub updated_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
/// Model capabilities.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CxModelCapabilities {
|
||||
pub chat: bool,
|
||||
pub streaming: bool,
|
||||
pub embedding: bool,
|
||||
pub vision: bool,
|
||||
pub code_gen: bool,
|
||||
pub function_calling: bool,
|
||||
pub max_context_tokens: Option<u32>,
|
||||
}
|
||||
|
||||
/// CxModel inference request with profile routing.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CxInferenceRequest {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub model: Option<String>,
|
||||
pub messages: Vec<super::ChatMessage>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub temperature: Option<f32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub max_tokens: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub profile: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub task: Option<String>,
|
||||
}
|
||||
|
||||
/// Inference profile matching .NET InferenceProfile.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InferenceProfile {
|
||||
pub id: String,
|
||||
pub provider: String,
|
||||
pub model: String,
|
||||
pub display: String,
|
||||
pub timeout_seconds: u64,
|
||||
pub tier: String,
|
||||
}
|
||||
|
||||
/// Model health report.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CxModelHealthReport {
|
||||
pub status: String,
|
||||
pub providers: Vec<ProviderHealth>,
|
||||
}
|
||||
|
||||
/// Individual provider health.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProviderHealth {
|
||||
pub name: String,
|
||||
pub status: String,
|
||||
pub latency_ms: Option<f64>,
|
||||
pub models_available: u32,
|
||||
}
|
||||
|
||||
/// Guardrail result from NVIDIA safety checks.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GuardrailResult {
|
||||
pub safe: bool,
|
||||
pub categories: Vec<String>,
|
||||
pub scores: Vec<f32>,
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
pub mod commissioning;
|
||||
pub mod common;
|
||||
pub mod energy;
|
||||
pub mod forma;
|
||||
pub mod inference;
|
||||
pub mod nemo;
|
||||
pub mod studio;
|
||||
pub mod telemetry;
|
||||
|
||||
pub use common::*;
|
||||
pub use inference::*;
|
||||
@@ -0,0 +1,120 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AgentRunRequest {
|
||||
pub goal: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub profile: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub session_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub max_steps: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AgentRunResponse {
|
||||
pub session_id: String,
|
||||
pub status: String,
|
||||
pub steps: Vec<AgentStep>,
|
||||
pub result: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AgentStep {
|
||||
pub step_number: u32,
|
||||
pub action: String,
|
||||
pub result: Option<String>,
|
||||
pub duration_ms: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NemoChatRequest {
|
||||
pub message: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub session_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub profile: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NemoChatResponse {
|
||||
pub reply: String,
|
||||
pub session_id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub profile: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NemoSession {
|
||||
pub id: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub messages: Vec<super::ChatMessage>,
|
||||
pub profile: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NemoSessionSummary {
|
||||
pub id: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub message_count: u32,
|
||||
pub last_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ToolDefinition {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub parameters: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ToolExecutionRequest {
|
||||
pub tool: String,
|
||||
pub arguments: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ToolExecutionResult {
|
||||
pub tool: String,
|
||||
pub success: bool,
|
||||
pub result: Option<serde_json::Value>,
|
||||
pub error: Option<String>,
|
||||
pub duration_ms: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RouteRequest {
|
||||
pub message: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub task_type: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RouteResponse {
|
||||
pub profile: super::InferenceProfile,
|
||||
pub confidence: f64,
|
||||
pub reasoning: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NemoMetrics {
|
||||
pub total_sessions: u64,
|
||||
pub active_sessions: u64,
|
||||
pub total_agent_runs: u64,
|
||||
pub avg_steps_per_run: f64,
|
||||
pub tool_executions: u64,
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct User {
|
||||
pub id: String,
|
||||
pub email: String,
|
||||
pub name: Option<String>,
|
||||
pub role: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Conversation {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
pub title: Option<String>,
|
||||
pub message_count: u32,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Deployment {
|
||||
pub id: String,
|
||||
pub service: String,
|
||||
pub version: String,
|
||||
pub status: String,
|
||||
pub target: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Service {
|
||||
pub name: String,
|
||||
pub port: u16,
|
||||
pub status: String,
|
||||
pub version: String,
|
||||
pub health: String,
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TelemetryEvent {
|
||||
pub event_type: String,
|
||||
pub service: String,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
pub data: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MemorySummary {
|
||||
pub total_mb: f64,
|
||||
pub used_mb: f64,
|
||||
pub gc_gen0: u64,
|
||||
pub gc_gen1: u64,
|
||||
pub gc_gen2: u64,
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
use backon::{ExponentialBuilder, Retryable};
|
||||
use std::future::Future;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::error::{CxError, CxResult};
|
||||
|
||||
/// Retry configuration matching .NET SDK retry/circuit-breaker policies.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RetryPolicy {
|
||||
pub max_attempts: u32,
|
||||
pub base_delay_ms: u64,
|
||||
pub max_delay_ms: u64,
|
||||
}
|
||||
|
||||
impl Default for RetryPolicy {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_attempts: 3,
|
||||
base_delay_ms: 200,
|
||||
max_delay_ms: 5000,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute an async operation with exponential backoff retry.
|
||||
pub async fn with_retry<F, Fut, T>(policy: &RetryPolicy, operation: F) -> CxResult<T>
|
||||
where
|
||||
F: FnMut() -> Fut,
|
||||
Fut: Future<Output = CxResult<T>>,
|
||||
{
|
||||
let backoff = ExponentialBuilder::default()
|
||||
.with_min_delay(std::time::Duration::from_millis(policy.base_delay_ms))
|
||||
.with_max_delay(std::time::Duration::from_millis(policy.max_delay_ms))
|
||||
.with_max_times(policy.max_attempts as usize);
|
||||
|
||||
operation
|
||||
.retry(backoff)
|
||||
.notify(|err: &CxError, dur| {
|
||||
warn!("Retrying after {dur:?} due to: {err}");
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn retry_succeeds_first_try() {
|
||||
let policy = RetryPolicy::default();
|
||||
let result = with_retry(&policy, || async { Ok::<_, CxError>(42) }).await;
|
||||
assert_eq!(result.unwrap(), 42);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user