Repository navigation
feat(llm)!: add native Perplexity Agent API support #62
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| //! Run a Google function call and replay its result through Perplexity. | ||
| //! | ||
| //! Set `PERPLEXITY_API_KEY`, then run: | ||
| //! `cargo run -p tinyinference-llm --example perplexity_function_replay`. | ||
| //! This makes two billable requests and runs one local demonstration function. | ||
|
|
||
| use serde_json::json; | ||
| use tinyinference_llm::{ | ||
| ChatModel, ContentBlock, ExecutionStatus, Message, ModelRequest, PerplexityModel, | ||
| PerplexitySelection, ToolSchema, message::ToolMessage, model::ToolChoice, | ||
| }; | ||
|
|
||
| #[tokio::main] | ||
| async fn main() -> Result<(), Box<dyn std::error::Error>> { | ||
| let key = std::env::var("PERPLEXITY_API_KEY")?; | ||
| let model = PerplexityModel::new( | ||
| key, | ||
| PerplexitySelection::Model("google/gemini-3.1-flash-lite".into()), | ||
| )?; | ||
| let tool = ToolSchema::new( | ||
| "lookup_code", | ||
| "Return the verification code for a record", | ||
| json!({ | ||
| "type": "object", | ||
| "properties": {"id": {"type": "integer"}}, | ||
| "required": ["id"], | ||
| "additionalProperties": false | ||
| }), | ||
| ); | ||
| let mut request = ModelRequest::new(vec![Message::user( | ||
| "Use lookup_code for record 7, then tell me its verification code.", | ||
| )]) | ||
| .with_tools(vec![tool]) | ||
| .with_tool_choice(ToolChoice::Tool("lookup_code".into())) | ||
| .with_max_tokens(512); | ||
| request.timeout_ms = Some(60_000); | ||
| let first = model.invoke(&(), request.clone()).await?; | ||
| let execution = first.execution.as_ref().ok_or("missing model identity")?; | ||
| if execution.status != ExecutionStatus::Completed || first.message.tool_calls.is_empty() { | ||
| return Err("expected a completed function call".into()); | ||
| } | ||
|
|
||
| // Replay the original assistant call so its argument bytes and signature survive. | ||
| request | ||
| .messages | ||
| .push(Message::Assistant(first.message.clone())); | ||
| for call in &first.message.tool_calls { | ||
| if call.invalid.is_some() | ||
| || call.name != "lookup_code" | ||
| || call.arguments.get("id").and_then(serde_json::Value::as_u64) != Some(7) | ||
| { | ||
| return Err("unexpected function or invalid arguments".into()); | ||
| } | ||
| let result = ToolMessage::for_call(call, vec![ContentBlock::Text("CODE_7391".into())]); | ||
| request.messages.push(Message::Tool(result)); | ||
| } | ||
| request.model = Some(execution.model.clone()); | ||
| request.continuation_id = None; | ||
| request.tool_choice = ToolChoice::None; | ||
| let response = model.invoke(&(), request).await?; | ||
| println!("{}", response.text()); | ||
| if !response.text().contains("CODE_7391") { | ||
| return Err("the model did not return the demonstration function result".into()); | ||
| } | ||
| Ok(()) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,7 @@ | |
|
|
||
| mod decorators; | ||
| pub mod discover; | ||
| mod output; | ||
| mod types; | ||
|
|
||
| #[cfg(test)] | ||
|
|
@@ -29,6 +30,7 @@ use crate::tool::{ToolCall, ToolSchema}; | |
| use crate::usage::Usage; | ||
|
|
||
| pub use decorators::*; | ||
| pub use output::*; | ||
| pub use types::*; | ||
|
|
||
| /// How a context-window pattern is matched against a model id. | ||
|
|
@@ -584,6 +586,8 @@ impl ModelResponse { | |
| /// Creates a response wrapping a plain assistant text message. | ||
| pub fn assistant(content: impl Into<String>) -> Self { | ||
| Self { | ||
| output: Vec::new(), | ||
| execution: None, | ||
| message: AssistantMessage { | ||
| id: None, | ||
| content: vec![ContentBlock::Text(content.into())], | ||
|
|
@@ -680,6 +684,9 @@ impl ModelResponse { | |
| /// usage. | ||
| #[derive(Clone, Debug, Default)] | ||
| pub struct StreamAccumulator { | ||
| output: std::collections::BTreeMap<usize, ModelOutputItem>, | ||
| execution: Option<ModelExecution>, | ||
| progress: Vec<ModelProgress>, | ||
| /// Concatenated text fragments. | ||
| text: String, | ||
| /// Accumulated reasoning/thinking fragments (side channel; not merged into | ||
|
|
@@ -714,6 +721,16 @@ impl StreamAccumulator { | |
| /// Folds one stream item into the running state. | ||
| pub fn push(&mut self, item: &ModelStreamItem) { | ||
| match item { | ||
| ModelStreamItem::OutputEvent(event) => match event { | ||
| ModelOutputEvent::Execution(execution) => { | ||
| self.execution = Some((**execution).clone()) | ||
| } | ||
| ModelOutputEvent::Item(item) => { | ||
| self.output.insert(item.index, (**item).clone()); | ||
| } | ||
| ModelOutputEvent::Progress(progress) => self.progress.push(progress.clone()), | ||
| _ => {} | ||
| }, | ||
| ModelStreamItem::Started => {} | ||
| ModelStreamItem::MessageDelta(delta) => { | ||
| self.text.push_str(&delta.text); | ||
|
|
@@ -824,6 +841,17 @@ impl StreamAccumulator { | |
| } | ||
|
|
||
| if let Some(mut response) = self.completed { | ||
| if response.execution.is_none() { | ||
| response.execution = self.execution; | ||
| } | ||
| if let Some(execution) = &mut response.execution | ||
| && execution.progress.is_empty() | ||
| { | ||
| execution.progress = self.progress; | ||
| } | ||
| if response.output.is_empty() { | ||
| response.output = self.output.into_values().collect(); | ||
| } | ||
| // Reconcile the response and message usage with any streamed | ||
| // `UsageDelta`, preferring an already-present value and never | ||
| // overwriting a known usage with `None` (which previously clobbered a | ||
|
|
@@ -870,6 +898,13 @@ impl StreamAccumulator { | |
| origin: None, | ||
| }; | ||
| Ok(ModelResponse { | ||
| output: self.output.into_values().collect(), | ||
| execution: self.execution.map(|mut execution| { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Preserve execution progress in reconstructed responses When a stream has an Additional
|
||
| if execution.progress.is_empty() { | ||
| execution.progress = self.progress; | ||
| } | ||
| execution | ||
| }), | ||
| message, | ||
| usage: self.usage, | ||
| finish_reason: None, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update all ToolMessage literals for the new required field
Adding a field to this public struct breaks existing struct literals that construct
ToolMessagewithoutcall_context(including production and test callers elsewhere in the crate). Rust requires every struct literal to specify the new field, so the crate will fail to compile as written. Update all callers in the same change, or redesign this API so adding context does not require every literal to change.Additional
critiqueobservationPreserve compatibility for existing ToolMessage literals
[RULE] breaking-api
Adding a field to this public struct requires every existing struct literal to initialize it. The repository still has
ToolMessage { ... }literals that do not providecall_context, including production code, so the workspace will fail to compile; downstream callers using the public struct literal syntax break as well. Update all construction sites (usingcall_context: Nonewhere appropriate), or provide a compatibility-preserving construction/API strategy before merging.[RULE] breaking-public-struct-change ·