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