vendor: update cargo-cxai-bridge-0.1.0
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"git": {
|
||||||
|
"sha1": "d878deb8441897ecdd416011b49d2d2f6112e867"
|
||||||
|
},
|
||||||
|
"path_in_vcs": "crates/cxai-bridge"
|
||||||
|
}
|
||||||
Generated
+2071
File diff suppressed because it is too large
Load Diff
+84
@@ -0,0 +1,84 @@
|
|||||||
|
# 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-bridge"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["CxAI-LLM <agent@cxai-studio.com>"]
|
||||||
|
build = false
|
||||||
|
autolib = false
|
||||||
|
autobins = false
|
||||||
|
autoexamples = false
|
||||||
|
autotests = false
|
||||||
|
autobenches = false
|
||||||
|
description = "FFI bridge for C++ interop, JSON-RPC proxy, and SignalR WebSocket client"
|
||||||
|
homepage = "https://cxllm.io"
|
||||||
|
readme = false
|
||||||
|
license = "MIT"
|
||||||
|
repository = "https://git.cxllm-studio.com/CxAI-LLM/CxAI.Rust"
|
||||||
|
resolver = "2"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "cxai_bridge"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[dependencies.cxai-sdk]
|
||||||
|
version = "0.1.0"
|
||||||
|
registry-index = "sparse+https://git.cxllm-studio.com/api/packages/CxAI-LLM/cargo/"
|
||||||
|
|
||||||
|
[dependencies.futures-util]
|
||||||
|
version = "0.3"
|
||||||
|
|
||||||
|
[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.tokio-tungstenite]
|
||||||
|
version = "0.26"
|
||||||
|
features = ["rustls-tls-webpki-roots"]
|
||||||
|
|
||||||
|
[dependencies.tracing]
|
||||||
|
version = "0.1"
|
||||||
|
|
||||||
|
[dependencies.uuid]
|
||||||
|
version = "1"
|
||||||
|
features = [
|
||||||
|
"v4",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[dev-dependencies.tokio]
|
||||||
|
version = "1"
|
||||||
|
features = [
|
||||||
|
"full",
|
||||||
|
"test-util",
|
||||||
|
"macros",
|
||||||
|
]
|
||||||
Generated
+24
@@ -0,0 +1,24 @@
|
|||||||
|
[package]
|
||||||
|
name = "cxai-bridge"
|
||||||
|
description = "FFI bridge for C++ interop, JSON-RPC proxy, and SignalR WebSocket client"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
homepage.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
cxai-sdk = { workspace = true }
|
||||||
|
tokio = { workspace = true }
|
||||||
|
reqwest = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
tracing = { workspace = true }
|
||||||
|
tokio-tungstenite = { workspace = true }
|
||||||
|
futures-util = { workspace = true }
|
||||||
|
uuid = { workspace = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio = { workspace = true, features = ["test-util", "macros"] }
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
pub mod pubsub;
|
||||||
|
pub mod rpc;
|
||||||
|
pub mod signalr;
|
||||||
|
|
||||||
|
pub use pubsub::PubSubClient;
|
||||||
|
pub use rpc::RpcClient;
|
||||||
|
pub use signalr::SignalRClient;
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::{RwLock, broadcast};
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
type MessageHandler = broadcast::Sender<serde_json::Value>;
|
||||||
|
|
||||||
|
/// In-process pub/sub hub matching the .NET PubSubService.
|
||||||
|
pub struct PubSubClient {
|
||||||
|
topics: Arc<RwLock<HashMap<String, MessageHandler>>>,
|
||||||
|
channel_capacity: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PubSubClient {
|
||||||
|
pub fn new(channel_capacity: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
topics: Arc::new(RwLock::new(HashMap::new())),
|
||||||
|
channel_capacity,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscribe to a topic, returning a receiver for messages.
|
||||||
|
pub async fn subscribe(&self, topic: &str) -> broadcast::Receiver<serde_json::Value> {
|
||||||
|
let mut topics = self.topics.write().await;
|
||||||
|
let sender = topics.entry(topic.to_string()).or_insert_with(|| {
|
||||||
|
debug!(topic, "Creating new topic channel");
|
||||||
|
let (tx, _) = broadcast::channel(self.channel_capacity);
|
||||||
|
tx
|
||||||
|
});
|
||||||
|
sender.subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Publish a message to a topic.
|
||||||
|
pub async fn publish(&self, topic: &str, message: serde_json::Value) -> usize {
|
||||||
|
let topics = self.topics.read().await;
|
||||||
|
if let Some(sender) = topics.get(topic) {
|
||||||
|
match sender.send(message) {
|
||||||
|
Ok(count) => {
|
||||||
|
debug!(topic, count, "Published message");
|
||||||
|
count
|
||||||
|
}
|
||||||
|
Err(_) => 0,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List all active topics.
|
||||||
|
pub async fn topics(&self) -> Vec<String> {
|
||||||
|
self.topics.read().await.keys().cloned().collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PubSubClient {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new(256)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn pubsub_roundtrip() {
|
||||||
|
let hub = PubSubClient::default();
|
||||||
|
let mut rx = hub.subscribe("test.topic").await;
|
||||||
|
let msg = serde_json::json!({"hello": "world"});
|
||||||
|
hub.publish("test.topic", msg.clone()).await;
|
||||||
|
let received = rx.recv().await.unwrap();
|
||||||
|
assert_eq!(received, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn pubsub_no_subscribers() {
|
||||||
|
let hub = PubSubClient::default();
|
||||||
|
let count = hub.publish("empty.topic", serde_json::json!({})).await;
|
||||||
|
assert_eq!(count, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
+106
@@ -0,0 +1,106 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tracing::{debug, error};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
/// JSON-RPC 2.0 client for the C++ bridge backend.
|
||||||
|
pub struct RpcClient {
|
||||||
|
http: reqwest::Client,
|
||||||
|
endpoint: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct JsonRpcRequest {
|
||||||
|
jsonrpc: &'static str,
|
||||||
|
method: String,
|
||||||
|
params: Option<serde_json::Value>,
|
||||||
|
id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct JsonRpcResponse {
|
||||||
|
result: Option<serde_json::Value>,
|
||||||
|
error: Option<JsonRpcError>,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct JsonRpcError {
|
||||||
|
code: i64,
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RpcClient {
|
||||||
|
/// Create a new JSON-RPC client pointing to the C++ bridge backend.
|
||||||
|
pub fn new(endpoint: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
http: reqwest::Client::new(),
|
||||||
|
endpoint: endpoint.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Call an RPC method with optional parameters.
|
||||||
|
pub async fn call(
|
||||||
|
&self,
|
||||||
|
method: &str,
|
||||||
|
params: Option<serde_json::Value>,
|
||||||
|
) -> Result<serde_json::Value, cxai_sdk::CxError> {
|
||||||
|
let request_id = Uuid::new_v4().to_string();
|
||||||
|
let rpc_req = JsonRpcRequest {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
method: method.to_string(),
|
||||||
|
params,
|
||||||
|
id: request_id.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!(method, id = %request_id, "RPC call");
|
||||||
|
|
||||||
|
let resp = self
|
||||||
|
.http
|
||||||
|
.post(&self.endpoint)
|
||||||
|
.json(&rpc_req)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(cxai_sdk::CxError::Http)?;
|
||||||
|
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
return Err(cxai_sdk::CxError::from_response(resp).await);
|
||||||
|
}
|
||||||
|
|
||||||
|
let rpc_resp: JsonRpcResponse = resp.json().await.map_err(cxai_sdk::CxError::Http)?;
|
||||||
|
|
||||||
|
if let Some(err) = rpc_resp.error {
|
||||||
|
error!(code = err.code, message = %err.message, "RPC error");
|
||||||
|
return Err(cxai_sdk::CxError::Api {
|
||||||
|
status: err.code as u16,
|
||||||
|
message: err.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
rpc_resp.result.ok_or_else(|| cxai_sdk::CxError::Api {
|
||||||
|
status: 500,
|
||||||
|
message: "RPC response missing result".into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Discover available modules and methods on the C++ server.
|
||||||
|
pub async fn discover(&self) -> Result<serde_json::Value, cxai_sdk::CxError> {
|
||||||
|
self.call("system.discover", None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Health check the C++ backend.
|
||||||
|
pub async fn health(&self) -> Result<serde_json::Value, cxai_sdk::CxError> {
|
||||||
|
self.call("system.health", None).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rpc_client_constructs() {
|
||||||
|
let client = RpcClient::new("http://localhost:9090/rpc");
|
||||||
|
assert_eq!(client.endpoint, "http://localhost:9090/rpc");
|
||||||
|
}
|
||||||
|
}
|
||||||
+128
@@ -0,0 +1,128 @@
|
|||||||
|
use futures_util::{SinkExt, StreamExt};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
use tokio_tungstenite::tungstenite::Message;
|
||||||
|
use tracing::{debug, error, info, warn};
|
||||||
|
|
||||||
|
/// SignalR WebSocket client for real-time bridge events.
|
||||||
|
pub struct SignalRClient {
|
||||||
|
url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct SignalRMessage {
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
msg_type: u8,
|
||||||
|
target: String,
|
||||||
|
arguments: Vec<serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct SignalRIncoming {
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
msg_type: u8,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
target: Option<String>,
|
||||||
|
arguments: Option<Vec<serde_json::Value>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SignalRClient {
|
||||||
|
/// Create a new SignalR client for the bridge hub.
|
||||||
|
pub fn new(base_url: impl Into<String>) -> Self {
|
||||||
|
let base = base_url.into();
|
||||||
|
let ws_url = base
|
||||||
|
.replace("http://", "ws://")
|
||||||
|
.replace("https://", "wss://");
|
||||||
|
Self {
|
||||||
|
url: format!("{ws_url}/hubs/bridge"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connect and subscribe to a topic, returning a channel of messages.
|
||||||
|
pub async fn subscribe(
|
||||||
|
&self,
|
||||||
|
topic: &str,
|
||||||
|
) -> Result<mpsc::Receiver<serde_json::Value>, cxai_sdk::CxError> {
|
||||||
|
let (tx, rx) = mpsc::channel(256);
|
||||||
|
let url = self.url.clone();
|
||||||
|
let topic = topic.to_string();
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
match tokio_tungstenite::connect_async(&url).await {
|
||||||
|
Ok((ws_stream, _)) => {
|
||||||
|
info!(url = %url, "SignalR connected");
|
||||||
|
let (mut write, mut read) = ws_stream.split();
|
||||||
|
|
||||||
|
// Send SignalR handshake
|
||||||
|
let handshake = "{\"protocol\":\"json\",\"version\":1}\x1e".to_string();
|
||||||
|
if let Err(e) = write.send(Message::Text(handshake.into())).await {
|
||||||
|
error!("Handshake failed: {e}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to topic
|
||||||
|
let sub_msg = SignalRMessage {
|
||||||
|
msg_type: 1,
|
||||||
|
target: "Subscribe".into(),
|
||||||
|
arguments: vec![serde_json::json!(topic)],
|
||||||
|
};
|
||||||
|
let payload = format!("{}\x1e", serde_json::to_string(&sub_msg).unwrap());
|
||||||
|
if let Err(e) = write.send(Message::Text(payload.into())).await {
|
||||||
|
error!("Subscribe failed: {e}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
debug!(topic = %topic, "Subscribed");
|
||||||
|
|
||||||
|
// Read messages
|
||||||
|
while let Some(msg) = read.next().await {
|
||||||
|
match msg {
|
||||||
|
Ok(Message::Text(text)) => {
|
||||||
|
for part in text.split('\x1e').filter(|s| !s.is_empty()) {
|
||||||
|
if let Ok(incoming) =
|
||||||
|
serde_json::from_str::<SignalRIncoming>(part)
|
||||||
|
&& incoming.msg_type == 1
|
||||||
|
&& let Some(args) = incoming.arguments
|
||||||
|
{
|
||||||
|
for arg in args {
|
||||||
|
if tx.send(arg).await.is_err() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Message::Close(_)) => {
|
||||||
|
info!("SignalR connection closed");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("WebSocket error: {e}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to connect: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(rx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn signalr_url_construction() {
|
||||||
|
let client = SignalRClient::new("http://localhost:9100");
|
||||||
|
assert_eq!(client.url, "ws://localhost:9100/hubs/bridge");
|
||||||
|
|
||||||
|
let client = SignalRClient::new("https://api.cxllm.io");
|
||||||
|
assert_eq!(client.url, "wss://api.cxllm.io/hubs/bridge");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user