Skip to content

Commit 70baa52

Browse files
committed
feat(read): add Read text-intelligence module
Phase 4 of the spec-coverage rollout — analyze plain text or a remote URL via POST /v1/read for sentiment, summary, topics, and intents. Public surface: ``` let response = dg.read() .analyze( &ReadRequest::text("Hello, world."), &Options::builder() .language("en") .summarize(true) .sentiment(true) .build(), ) .await?; if let Some(text) = response.summary_text() { println!("Summary: {text}"); } ``` Notable wire-shape quirks (matching the spec, the Python SDK, and the JS SDK — all auto-generated from the same OpenAPI definition): - metadata is double-wrapped: `metadata.metadata.{request_id, ...}`. Use ReadResponse::metadata_inner() to skip the wrapper. - summary text is four levels deep: `results.summary.results. summary.text`. Use ReadResponse::summary_text() to climb it. Topics / Intents / Sentiments reuse the existing types from common::batch_response since the spec's schemas.shared.yml definitions are shared between Listen and Read. The `common` mod gate is widened from cfg(feature = "listen") to cfg(any(feature = "listen", feature = "read")). The new `read` Cargo feature is REST-only (no tungstenite deps). auth/base_url/client cfg_attrs updated to allow read, with a comment explaining why auth specifically is left listen+agent+speak only (auth is baked into reqwest default headers at construction and never re-read by REST code paths). Three examples under examples/read/: sentiment_url, summarize_text, intents_topics. 12 new tests pass; 140 total. All feature combinations build clean.
1 parent df0fe99 commit 70baa52

10 files changed

Lines changed: 975 additions & 3 deletions

File tree

Cargo.toml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ manage = []
6060
listen = ["dep:tungstenite", "dep:tokio-tungstenite"]
6161
speak = ["dep:tungstenite", "dep:tokio-tungstenite"]
6262
agent = ["dep:tungstenite", "dep:tokio-tungstenite"]
63+
read = []
6364

6465
[[example]]
6566
name = "grant_token"
@@ -150,6 +151,21 @@ name = "speak_websocket_flush_clear"
150151
path = "examples/speak/websocket/flush_clear.rs"
151152
required-features = ["speak"]
152153

154+
[[example]]
155+
name = "read_sentiment_url"
156+
path = "examples/read/sentiment_url.rs"
157+
required-features = ["read"]
158+
159+
[[example]]
160+
name = "read_summarize_text"
161+
path = "examples/read/summarize_text.rs"
162+
required-features = ["read"]
163+
164+
[[example]]
165+
name = "read_intents_topics"
166+
path = "examples/read/intents_topics.rs"
167+
required-features = ["read"]
168+
153169
[[example]]
154170
name = "agent_simple"
155171
path = "examples/agent/websocket/simple_agent.rs"

examples/read/intents_topics.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/* Expected result from running this example program.
2+
Request ID: <uuid>
3+
--- Topics ---
4+
travel
5+
recommendation
6+
--- Intents ---
7+
ask_recommendation
8+
*/
9+
10+
//! Read API: run topics + intents on inline text, with custom topics
11+
//! and intents to demonstrate the `extended` and `strict` modes.
12+
//!
13+
//! Run with:
14+
//!
15+
//! ```bash
16+
//! DEEPGRAM_API_KEY=<your-key> \
17+
//! cargo run --features read --example read_intents_topics
18+
//! ```
19+
20+
use std::env;
21+
22+
use deepgram::common::options::{CustomIntentMode, CustomTopicMode};
23+
use deepgram::read::{options::Options, request::ReadRequest};
24+
use deepgram::{Deepgram, DeepgramError};
25+
26+
const TEXT: &str = "Hi! I'm planning a trip to Tokyo next month and I'd love any \
27+
restaurant recommendations near Shibuya. Also, what's the best \
28+
way to get from Narita Airport to the city?";
29+
30+
#[tokio::main]
31+
async fn main() -> Result<(), DeepgramError> {
32+
let api_key = env::var("DEEPGRAM_API_KEY").expect("DEEPGRAM_API_KEY environment variable");
33+
let dg = Deepgram::new(&api_key)?;
34+
35+
let options = Options::builder()
36+
.language("en")
37+
.topics(true)
38+
.custom_topics(["travel", "recommendation"])
39+
.custom_topic_mode(CustomTopicMode::Extended)
40+
.intents(true)
41+
.custom_intents(["ask_recommendation"])
42+
.custom_intent_mode(CustomIntentMode::Strict)
43+
.build();
44+
45+
let response = dg
46+
.read()
47+
.analyze(&ReadRequest::text(TEXT), &options)
48+
.await?;
49+
50+
if let Some(meta) = response.metadata_inner() {
51+
if let Some(id) = &meta.request_id {
52+
println!("Request ID: {id}");
53+
}
54+
}
55+
56+
println!("--- Topics ---");
57+
if let Some(topics) = response.results.topics.as_ref() {
58+
let v = serde_json::to_value(topics)?;
59+
if let Some(segments) = v.get("segments").and_then(|s| s.as_array()) {
60+
for seg in segments {
61+
if let Some(arr) = seg.get("topics").and_then(|t| t.as_array()) {
62+
for topic in arr {
63+
if let Some(t) = topic.get("topic").and_then(|t| t.as_str()) {
64+
println!(" {t}");
65+
}
66+
}
67+
}
68+
}
69+
}
70+
} else {
71+
println!(" (none)");
72+
}
73+
74+
println!("--- Intents ---");
75+
if let Some(intents) = response.results.intents.as_ref() {
76+
let v = serde_json::to_value(intents)?;
77+
if let Some(segments) = v.get("segments").and_then(|s| s.as_array()) {
78+
for seg in segments {
79+
if let Some(arr) = seg.get("intents").and_then(|i| i.as_array()) {
80+
for intent in arr {
81+
if let Some(name) = intent.get("intent").and_then(|n| n.as_str()) {
82+
println!(" {name}");
83+
}
84+
}
85+
}
86+
}
87+
}
88+
} else {
89+
println!(" (none)");
90+
}
91+
92+
Ok(())
93+
}

examples/read/sentiment_url.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/* Expected result from running this example program.
2+
Request ID: <uuid>
3+
Language: en
4+
Sentiment: positive (avg 0.42)
5+
*/
6+
7+
//! Read API: fetch a remote document and run sentiment analysis on it.
8+
//!
9+
//! Run with:
10+
//!
11+
//! ```bash
12+
//! DEEPGRAM_API_KEY=<your-key> \
13+
//! cargo run --features read --example read_sentiment_url
14+
//! ```
15+
16+
use std::env;
17+
18+
use deepgram::read::{options::Options, request::ReadRequest};
19+
use deepgram::{Deepgram, DeepgramError};
20+
21+
#[tokio::main]
22+
async fn main() -> Result<(), DeepgramError> {
23+
let api_key = env::var("DEEPGRAM_API_KEY").expect("DEEPGRAM_API_KEY environment variable");
24+
let dg = Deepgram::new(&api_key)?;
25+
26+
let options = Options::builder().language("en").sentiment(true).build();
27+
28+
let response = dg
29+
.read()
30+
.analyze(
31+
&ReadRequest::url(
32+
"https://static.deepgram.com/examples/Bueller-Life-moves-pretty-fast.txt",
33+
),
34+
&options,
35+
)
36+
.await?;
37+
38+
if let Some(meta) = response.metadata_inner() {
39+
if let Some(id) = &meta.request_id {
40+
println!("Request ID: {id}");
41+
}
42+
if let Some(lang) = &meta.language {
43+
println!("Language: {lang}");
44+
}
45+
}
46+
47+
if let Some(sentiments) = response.results.sentiments.as_ref() {
48+
// Sentiments has private internal fields; surface what serde gives us.
49+
let v = serde_json::to_value(sentiments)?;
50+
if let Some(avg) = v.get("average") {
51+
let label = avg.get("sentiment").and_then(|s| s.as_str()).unwrap_or("?");
52+
let score = avg
53+
.get("sentiment_score")
54+
.and_then(|s| s.as_f64())
55+
.unwrap_or(0.0);
56+
println!("Sentiment: {label} (avg {score:.2})");
57+
}
58+
} else {
59+
println!("No sentiment data returned.");
60+
}
61+
62+
Ok(())
63+
}

examples/read/summarize_text.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/* Expected result from running this example program.
2+
Request ID: <uuid>
3+
Summary: A condensed paraphrase of the input text.
4+
*/
5+
6+
//! Read API: pass an inline string and get back a summary.
7+
//!
8+
//! Run with:
9+
//!
10+
//! ```bash
11+
//! DEEPGRAM_API_KEY=<your-key> \
12+
//! cargo run --features read --example read_summarize_text
13+
//! ```
14+
15+
use std::env;
16+
17+
use deepgram::read::{options::Options, request::ReadRequest};
18+
use deepgram::{Deepgram, DeepgramError};
19+
20+
const TEXT: &str = "Deepgram's Voice AI platform powers speech-to-text, text-to-speech, \
21+
and full conversational agents. The Read API analyzes text content \
22+
using the same intelligence features (sentiment, summarize, topics, \
23+
intents) that the Listen API exposes for audio. This means you can \
24+
process transcripts, documents, or any plain text uniformly without \
25+
spinning up an audio pipeline.";
26+
27+
#[tokio::main]
28+
async fn main() -> Result<(), DeepgramError> {
29+
let api_key = env::var("DEEPGRAM_API_KEY").expect("DEEPGRAM_API_KEY environment variable");
30+
let dg = Deepgram::new(&api_key)?;
31+
32+
let options = Options::builder().language("en").summarize(true).build();
33+
34+
let response = dg
35+
.read()
36+
.analyze(&ReadRequest::text(TEXT), &options)
37+
.await?;
38+
39+
if let Some(meta) = response.metadata_inner() {
40+
if let Some(id) = &meta.request_id {
41+
println!("Request ID: {id}");
42+
}
43+
}
44+
45+
match response.summary_text() {
46+
Some(text) => println!("Summary: {text}"),
47+
None => println!("No summary returned."),
48+
}
49+
50+
Ok(())
51+
}

src/lib.rs

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,14 @@ use url::Url;
2727
#[cfg(feature = "agent")]
2828
pub mod agent;
2929
pub mod auth;
30-
#[cfg(feature = "listen")]
30+
#[cfg(any(feature = "listen", feature = "read"))]
3131
pub mod common;
3232
#[cfg(feature = "listen")]
3333
pub mod listen;
3434
#[cfg(feature = "manage")]
3535
pub mod manage;
36+
#[cfg(feature = "read")]
37+
pub mod read;
3638
#[cfg(feature = "speak")]
3739
pub mod speak;
3840

@@ -65,6 +67,17 @@ pub struct Transcription<'a>(#[allow(unused)] pub &'a Deepgram);
6567
#[derive(Debug, Clone)]
6668
pub struct Speak<'a>(#[allow(unused)] pub &'a Deepgram);
6769

70+
/// Analyze text using Deepgram's text intelligence API.
71+
///
72+
/// Constructed using [`Deepgram::read`].
73+
///
74+
/// See the [Deepgram API Reference][api] for more info.
75+
///
76+
/// [api]: https://developers.deepgram.com/reference/read-api
77+
#[cfg(feature = "read")]
78+
#[derive(Debug, Clone)]
79+
pub struct Read<'a>(#[allow(unused)] pub &'a Deepgram);
80+
6881
impl Deepgram {
6982
/// Construct a new [`Transcription`] from a [`Deepgram`].
7083
pub fn transcription(&self) -> Transcription<'_> {
@@ -83,6 +96,15 @@ impl Deepgram {
8396
pub fn agent(&self) -> crate::agent::Agent<'_> {
8497
self.into()
8598
}
99+
100+
/// Construct a new [`Read`] sub-client.
101+
///
102+
/// Use this to analyze text via Deepgram's `/v1/read` endpoint
103+
/// (sentiment, summarize, topics, intents).
104+
#[cfg(feature = "read")]
105+
pub fn read(&self) -> Read<'_> {
106+
self.into()
107+
}
86108
}
87109

88110
impl<'a> From<&'a Deepgram> for Transcription<'a> {
@@ -99,6 +121,13 @@ impl<'a> From<&'a Deepgram> for Speak<'a> {
99121
}
100122
}
101123

124+
#[cfg(feature = "read")]
125+
impl<'a> From<&'a Deepgram> for Read<'a> {
126+
fn from(deepgram: &'a Deepgram) -> Self {
127+
Self(deepgram)
128+
}
129+
}
130+
102131
impl Transcription<'_> {
103132
/// Expose a method to access the inner `Deepgram` reference if needed.
104133
pub fn deepgram(&self) -> &Deepgram {
@@ -151,14 +180,24 @@ impl AuthMethod {
151180
/// Make transcriptions requests using [`Deepgram::transcription`].
152181
#[derive(Debug, Clone)]
153182
pub struct Deepgram {
183+
// `auth` is consumed by `inner_constructor` to bake the
184+
// Authorization header into the reqwest client's default headers.
185+
// Only the WS code paths in listen/agent/speak read the field
186+
// afterwards; the REST code paths (including `read`) don't.
154187
#[cfg_attr(
155188
not(any(feature = "listen", feature = "agent", feature = "speak")),
156189
allow(unused)
157190
)]
158191
auth: Option<AuthMethod>,
159-
#[cfg_attr(not(feature = "listen"), allow(unused))]
192+
#[cfg_attr(
193+
not(any(feature = "listen", feature = "speak", feature = "read")),
194+
allow(unused)
195+
)]
160196
base_url: Url,
161-
#[cfg_attr(not(feature = "listen"), allow(unused))]
197+
#[cfg_attr(
198+
not(any(feature = "listen", feature = "speak", feature = "read")),
199+
allow(unused)
200+
)]
162201
client: reqwest::Client,
163202
}
164203

src/read/mod.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
//! Read API — analyze plain text or a remote document for sentiment,
2+
//! summary, topics, and intents.
3+
//!
4+
//! Mirrors `POST /v1/read` (`openapi/paths/read.v1.yml`).
5+
//!
6+
//! Entry point: [`crate::Deepgram::read`].
7+
//!
8+
//! ```no_run
9+
//! use deepgram::Deepgram;
10+
//! use deepgram::read::{options::Options, request::ReadRequest};
11+
//!
12+
//! # async fn run() -> Result<(), deepgram::DeepgramError> {
13+
//! let dg = Deepgram::new(std::env::var("DEEPGRAM_API_KEY").unwrap_or_default())?;
14+
//! let options = Options::builder()
15+
//! .language("en")
16+
//! .sentiment(true)
17+
//! .summarize(true)
18+
//! .topics(true)
19+
//! .build();
20+
//!
21+
//! let response = dg
22+
//! .read()
23+
//! .analyze(&ReadRequest::text("Deepgram makes voice AI fast and easy."), &options)
24+
//! .await?;
25+
//!
26+
//! if let Some(text) = response.summary_text() {
27+
//! println!("Summary: {text}");
28+
//! }
29+
//! # Ok(())
30+
//! # }
31+
//! ```
32+
33+
pub mod options;
34+
pub mod request;
35+
pub mod response;
36+
pub mod rest;
37+
38+
pub use options::{Options, OptionsBuilder};
39+
pub use request::ReadRequest;
40+
pub use response::{
41+
ReadMetadata, ReadMetadataWrapper, ReadResponse, ReadResults, ReadSummaryInner,
42+
ReadSummaryText, ReadSummaryWrapper, TokenInfo,
43+
};

0 commit comments

Comments
 (0)