Skip to content

Commit 1e6ae8d

Browse files
committed
chore: improve RequestMeta
1 parent f1afb93 commit 1e6ae8d

File tree

5 files changed

+155
-12
lines changed

5 files changed

+155
-12
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ members = [
1212
]
1313

1414
[workspace.package]
15-
version = "0.9.2"
15+
version = "0.9.5"
1616
description = "Anda is an AI agent framework built with Rust, powered by ICP and TEEs."
1717
repository = "https://github.com/ldclabs/anda"
1818
homepage = "https://github.com/ldclabs/anda"

anda_core/src/model.rs

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
1111
use candid::Principal;
1212
use serde::{Deserialize, Serialize};
13-
use serde_json::json;
13+
use serde_json::{Map, json};
1414
use std::collections::BTreeMap;
1515

1616
use crate::Json;
@@ -291,6 +291,7 @@ impl<T> ToolOutput<T> {
291291
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
292292
pub struct RequestMeta {
293293
/// The target engine principal for the request.
294+
#[serde(skip_serializing_if = "Option::is_none")]
294295
pub engine: Option<Principal>,
295296

296297
/// Gets the username from request context.
@@ -299,6 +300,11 @@ pub struct RequestMeta {
299300
/// of the user interacting with the bot.
300301
#[serde(skip_serializing_if = "Option::is_none")]
301302
pub user: Option<String>,
303+
304+
/// Extra metadata key-value pairs.
305+
#[serde(flatten)]
306+
#[serde(skip_serializing_if = "Map::is_empty")]
307+
pub extra: Map<String, Json>,
302308
}
303309

304310
/// Represents the usage statistics for the agent or tool execution.
@@ -742,4 +748,65 @@ mod tests {
742748
assert_eq!(calls[1].name, "echo");
743749
assert_eq!(calls[1].args, serde_json::json!({"text":"hi"}));
744750
}
751+
752+
#[test]
753+
fn test_request_meta_extra_flatten_serde() {
754+
// empty extra should not serialize
755+
let meta = RequestMeta {
756+
engine: None,
757+
user: None,
758+
extra: Map::new(),
759+
};
760+
let v = serde_json::to_value(&meta).unwrap();
761+
assert_eq!(v, serde_json::json!({}));
762+
763+
// extra should be flattened into the top-level object
764+
let mut extra = Map::new();
765+
extra.insert("foo".into(), serde_json::json!("bar"));
766+
extra.insert("n".into(), serde_json::json!(1));
767+
extra.insert("obj".into(), serde_json::json!({"x": true}));
768+
769+
let meta2 = RequestMeta {
770+
engine: Some(Principal::from_text("aaaaa-aa").unwrap()),
771+
user: Some("alice".into()),
772+
extra,
773+
};
774+
775+
let v2 = serde_json::to_value(&meta2).unwrap();
776+
assert_eq!(v2.get("engine").unwrap(), "aaaaa-aa");
777+
assert_eq!(v2.get("user").unwrap(), "alice");
778+
assert_eq!(v2.get("foo").unwrap(), "bar");
779+
assert_eq!(v2.get("n").unwrap(), 1);
780+
assert_eq!(v2.get("obj").unwrap(), &serde_json::json!({"x": true}));
781+
assert!(v2.get("extra").is_none());
782+
783+
// deserialization: unknown fields go into extra
784+
let input = serde_json::json!({
785+
"engine": "aaaaa-aa",
786+
"user": "bob",
787+
"k1": "v1",
788+
"k2": 2,
789+
"nested": {"a": 1}
790+
});
791+
let back: RequestMeta = serde_json::from_value(input).unwrap();
792+
assert_eq!(back.engine.unwrap().to_text(), "aaaaa-aa");
793+
assert_eq!(back.user.as_deref(), Some("bob"));
794+
assert_eq!(back.extra.get("k1").unwrap(), "v1");
795+
assert_eq!(back.extra.get("k2").unwrap(), 2);
796+
assert_eq!(
797+
back.extra.get("nested").unwrap(),
798+
&serde_json::json!({"a": 1})
799+
);
800+
801+
// round-trip (field-by-field)
802+
let back2: RequestMeta = serde_json::from_value(v2).unwrap();
803+
assert_eq!(back2.engine.unwrap().to_text(), "aaaaa-aa");
804+
assert_eq!(back2.user.as_deref(), Some("alice"));
805+
assert_eq!(back2.extra.get("foo").unwrap(), "bar");
806+
assert_eq!(back2.extra.get("n").unwrap(), 1);
807+
assert_eq!(
808+
back2.extra.get("obj").unwrap(),
809+
&serde_json::json!({"x": true})
810+
);
811+
}
745812
}

anda_engine/src/context/base.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ impl BaseCtx {
194194
RequestMeta {
195195
engine: Some(target),
196196
user: Some(self.name.clone()),
197+
extra: Default::default(),
197198
}
198199
}
199200

anda_engine_server/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name = "anda_engine_server"
33
description = "A http server to serve multiple Anda engines."
44
repository = "https://github.com/ldclabs/anda/tree/main/anda_engine_server"
55
publish = true
6-
version = "0.9.4"
6+
version = "0.9.5"
77
edition.workspace = true
88
keywords.workspace = true
99
categories.workspace = true
@@ -16,6 +16,7 @@ axum = { workspace = true }
1616
candid = { workspace = true }
1717
ciborium = { workspace = true }
1818
serde = { workspace = true }
19+
serde_json = { workspace = true }
1920
http = { workspace = true }
2021
ic_cose_types = { workspace = true }
2122
ic_tee_agent = { workspace = true }

anda_engine_server/src/handler.rs

Lines changed: 83 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -130,16 +130,15 @@ pub async fn anda_engine(
130130
ANONYMOUS_PRINCIPAL
131131
};
132132

133-
log::info!(
134-
method = req.method.as_str(),
135-
agent = id.to_text(),
136-
caller = caller.to_text();
137-
"anda_engine",
138-
);
139-
let res = engine_run(req, &app, caller, id).await;
140133
match &ct {
141-
ContentWithSHA3::CBOR(_, _) => Content::CBOR(res, None).into_response(),
142-
ContentWithSHA3::JSON(_, _) => Content::JSON(res, None).into_response(),
134+
ContentWithSHA3::CBOR(_, _) => {
135+
let res = engine_run(req, &app, caller, id).await;
136+
Content::CBOR(res, None).into_response()
137+
}
138+
ContentWithSHA3::JSON(_, _) => {
139+
let res = engine_run_in_json(req, &app, caller, id).await;
140+
Content::JSON(res, None).into_response()
141+
}
143142
}
144143
}
145144

@@ -158,6 +157,13 @@ async fn engine_run(
158157
"agent_run" => {
159158
let args: (AgentInput,) = from_reader(req.params.as_slice())
160159
.map_err(|err| format!("failed to decode params: {err:?}"))?;
160+
log::info!(
161+
method = req.method.as_str(),
162+
agent = id.to_text(),
163+
caller = caller.to_text(),
164+
name = args.0.name;
165+
"",
166+
);
161167
let res = engine
162168
.agent_run(caller, args.0)
163169
.await
@@ -167,6 +173,13 @@ async fn engine_run(
167173
"tool_call" => {
168174
let args: (ToolInput<Json>,) = from_reader(req.params.as_slice())
169175
.map_err(|err| format!("failed to decode params: {err:?}"))?;
176+
log::info!(
177+
method = req.method.as_str(),
178+
agent = id.to_text(),
179+
caller = caller.to_text(),
180+
name = args.0.name;
181+
"",
182+
);
170183
let res = engine
171184
.tool_call(caller, args.0)
172185
.await
@@ -183,3 +196,64 @@ async fn engine_run(
183196
)),
184197
}
185198
}
199+
200+
async fn engine_run_in_json(
201+
req: &RPCRequest,
202+
app: &AppState,
203+
caller: Principal,
204+
id: Principal,
205+
) -> RPCResponse {
206+
let engine = app
207+
.engines
208+
.get(&id)
209+
.ok_or_else(|| format!("engine {} not found", id.to_text()))?;
210+
211+
match req.method.as_str() {
212+
"agent_run" => {
213+
let args: (AgentInput,) = serde_json::from_slice(req.params.as_slice())
214+
.map_err(|err| format!("failed to decode params: {err:?}"))?;
215+
log::info!(
216+
method = req.method.as_str(),
217+
agent = id.to_text(),
218+
caller = caller.to_text(),
219+
name = args.0.name;
220+
"",
221+
);
222+
let res = engine
223+
.agent_run(caller, args.0)
224+
.await
225+
.map_err(|err| format!("failed to run agent: {err:?}"))?;
226+
Ok(serde_json::to_vec(&res)
227+
.map_err(|err| format!("{:?}", err))?
228+
.into())
229+
}
230+
"tool_call" => {
231+
let args: (ToolInput<Json>,) = serde_json::from_slice(req.params.as_slice())
232+
.map_err(|err| format!("failed to decode params: {err:?}"))?;
233+
log::info!(
234+
method = req.method.as_str(),
235+
agent = id.to_text(),
236+
caller = caller.to_text(),
237+
name = args.0.name;
238+
"",
239+
);
240+
let res = engine
241+
.tool_call(caller, args.0)
242+
.await
243+
.map_err(|err| format!("failed to call tool: {err:?}"))?;
244+
Ok(serde_json::to_vec(&res)
245+
.map_err(|err| format!("{:?}", err))?
246+
.into())
247+
}
248+
"information" => {
249+
let res = engine.information();
250+
Ok(serde_json::to_vec(&res)
251+
.map_err(|err| format!("{:?}", err))?
252+
.into())
253+
}
254+
method => Err(format!(
255+
"{method} on engine {} not implemented",
256+
id.to_text()
257+
)),
258+
}
259+
}

0 commit comments

Comments
 (0)