Skip to content

Commit aaf8ca3

Browse files
committed
Add source-linked briefing example
1 parent cb90e63 commit aaf8ca3

5 files changed

Lines changed: 479 additions & 0 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ api.available_regions()
5757
api.available_category()
5858
```
5959

60+
## Examples
61+
62+
- [Generate a source-linked news briefing](examples/source_linked_briefing/README.md) from a live Search API response or a deterministic offline fixture.
63+
6064
## Authentication
6165

6266
All requests are authenticated using an `Authorization` header. Pass your API key when instantiating the client:
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Source-linked news briefing
2+
3+
This example turns a Currents Search API response into two local files:
4+
5+
- `briefing.md` for a person to read;
6+
- `briefing.json` for a dashboard, queue, or application-owned agent workflow.
7+
8+
Every item keeps its publisher URL and publication time. The script does not call a language model, summarize full publisher articles, or make decisions for the reader.
9+
10+
The example accompanies [Build a Source-Linked News Briefing with Currents Search API](https://currentsapi.services/en/blog/build-source-linked-news-briefing-currents-search-api).
11+
12+
## Run the checked-in fixture
13+
14+
Install the SDK from this checkout:
15+
16+
```bash
17+
python -m pip install -e .
18+
```
19+
20+
Generate deterministic output without a network request or API key:
21+
22+
```bash
23+
python examples/source_linked_briefing/briefing.py \
24+
--fixture examples/source_linked_briefing/fixtures/search_response.json \
25+
--output-dir briefing-output
26+
```
27+
28+
The fixture contains fictional `example.com` articles. It contains no customer data, publisher article bodies, or credentials.
29+
30+
## Run a live search
31+
32+
Create a [free Currents API key](https://currentsapi.services/en/register), then export it:
33+
34+
```bash
35+
export CURRENTS_API_KEY="your-api-key"
36+
```
37+
38+
Run a live search:
39+
40+
```bash
41+
python examples/source_linked_briefing/briefing.py \
42+
--keywords "energy storage" \
43+
--language en \
44+
--output-dir briefing-output
45+
```
46+
47+
Live mode calls `CurrentsAPI.search()`. Fixture mode reads a saved Search API response. Both modes use the same validation, normalization, ordering, and rendering path.
48+
49+
## Output
50+
51+
Articles are ordered by publication time, newest first. Titles provide a stable tie-breaker. Missing optional fields become empty values rather than fabricated content.
52+
53+
The Markdown output remains deliberately plain:
54+
55+
```markdown
56+
# Source-Linked News Briefing
57+
58+
Generated at: 2026-07-25T00:00:00+00:00
59+
60+
- Battery storage policy enters public consultation - <https://example.com/energy/storage-policy>
61+
- Published: 2026-07-25T08:00:00Z
62+
- A fictional example describing a public policy consultation.
63+
```
64+
65+
The JSON output contains `generated_at` and a normalized `articles` list. Each article can contain:
66+
67+
- `title`
68+
- `description`
69+
- `url`
70+
- `published`
71+
- `language`
72+
- `category`
73+
74+
## Where an agent fits
75+
76+
Currents retrieves structured news results and preserves source context. Your application owns any later prompt, model call, summary, alert, embedding, storage policy, or human-review step.
77+
78+
If you pass the JSON output to a model, instruct it to use only the supplied items, preserve every source URL, distinguish source facts from inference, and state when the retrieved context is insufficient.
79+
80+
## Limitations
81+
82+
- Search results reflect the index at request time. A saved briefing is not automatically refreshed.
83+
- Network failures, invalid credentials, plan limits, and rate limits can stop live execution.
84+
- Overlapping searches can return duplicate articles. This single-query example does not deduplicate across runs.
85+
- Descriptions can be missing or truncated. Follow the publisher URL for the source context.
86+
- Access through an API does not grant republication rights. Follow your Currents plan terms and publisher requirements.
87+
- Do not treat generated or retrieved text as investment, legal, medical, or other high-stakes advice.
88+
- Add retries, caching, incremental date windows, observability, and human review before scheduling production workloads.
89+
90+
## Test
91+
92+
Run the focused offline tests:
93+
94+
```bash
95+
python -m pytest tests/test_source_linked_briefing_example.py
96+
```
97+
98+
The tests cover source preservation, publication times, deterministic ordering, malformed responses, the checked-in fixture, and the live SDK adapter.
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
#!/usr/bin/env python3
2+
"""Generate a source-linked news briefing from Currents Search API data."""
3+
4+
import argparse
5+
import json
6+
import os
7+
from datetime import datetime, timezone
8+
from pathlib import Path
9+
10+
11+
def parse_args():
12+
parser = argparse.ArgumentParser(description=__doc__)
13+
parser.add_argument("--fixture", type=Path, help="Read a saved Search API response.")
14+
parser.add_argument("--keywords", default="artificial intelligence")
15+
parser.add_argument("--language", default="en")
16+
parser.add_argument("--output-dir", type=Path, default=Path("briefing-output"))
17+
parser.add_argument("--generated-at", help=argparse.SUPPRESS)
18+
return parser.parse_args()
19+
20+
21+
def normalize_article(article):
22+
return {
23+
"title": article.get("title") or "Untitled",
24+
"description": article.get("description") or "",
25+
"url": article.get("url") or "",
26+
"published": article.get("published") or "",
27+
"language": article.get("language") or "",
28+
"category": article.get("category") or [],
29+
}
30+
31+
32+
def load_fixture(path):
33+
return json.loads(path.read_text(encoding="utf-8"))
34+
35+
36+
def load_live_response(keywords, language):
37+
api_key = os.environ.get("CURRENTS_API_KEY")
38+
if not api_key:
39+
raise ValueError("CURRENTS_API_KEY is required for live mode")
40+
41+
from currentsapi import CurrentsAPI
42+
43+
return CurrentsAPI(api_key=api_key).search(
44+
keywords=keywords,
45+
language=language,
46+
)
47+
48+
49+
def validate_response(response):
50+
if not isinstance(response, dict):
51+
raise ValueError("Search API response must be an object")
52+
if response.get("status") != "ok":
53+
raise ValueError("Search API response status must be 'ok'")
54+
if not isinstance(response.get("news"), list):
55+
raise ValueError("Search API response news must be a list")
56+
if any(not isinstance(article, dict) for article in response["news"]):
57+
raise ValueError("Every news item must be an object")
58+
59+
60+
def build_output(response, generated_at):
61+
validate_response(response)
62+
articles = [normalize_article(article) for article in response["news"]]
63+
articles.sort(key=lambda article: article["title"].casefold())
64+
articles.sort(key=lambda article: article["published"], reverse=True)
65+
lines = ["# Source-Linked News Briefing", "", "Generated at: {}".format(generated_at), ""]
66+
for article in articles:
67+
title = article["title"]
68+
url = article["url"]
69+
lines.append("- {} - <{}>".format(title, url) if url else "- {}".format(title))
70+
if article["published"]:
71+
lines.append(" - Published: {}".format(article["published"]))
72+
if article["description"]:
73+
lines.append(" - {}".format(article["description"]))
74+
return "\n".join(lines) + "\n", {
75+
"generated_at": generated_at,
76+
"articles": articles,
77+
}
78+
79+
80+
def main():
81+
args = parse_args()
82+
83+
try:
84+
response = (
85+
load_fixture(args.fixture)
86+
if args.fixture
87+
else load_live_response(args.keywords, args.language)
88+
)
89+
generated_at = args.generated_at or datetime.now(timezone.utc).isoformat()
90+
markdown, structured = build_output(response, generated_at)
91+
except (OSError, ValueError, json.JSONDecodeError) as exc:
92+
raise SystemExit("error: {}".format(exc))
93+
94+
args.output_dir.mkdir(parents=True, exist_ok=True)
95+
(args.output_dir / "briefing.md").write_text(markdown, encoding="utf-8")
96+
(args.output_dir / "briefing.json").write_text(
97+
json.dumps(structured, indent=2) + "\n",
98+
encoding="utf-8",
99+
)
100+
print("Wrote {}".format(args.output_dir))
101+
102+
103+
if __name__ == "__main__":
104+
main()
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"status": "ok",
3+
"news": [
4+
{
5+
"id": "example-market-001",
6+
"title": "Grid operator announces storage procurement",
7+
"description": "A fictional example describing a new storage procurement round.",
8+
"url": "https://example.com/energy/storage-procurement",
9+
"published": "2026-07-23T14:15:00Z",
10+
"language": "en",
11+
"category": [
12+
"business"
13+
]
14+
},
15+
{
16+
"id": "example-policy-001",
17+
"title": "Battery storage policy enters public consultation",
18+
"description": "A fictional example describing a public policy consultation.",
19+
"url": "https://example.com/energy/storage-policy",
20+
"published": "2026-07-25T08:00:00Z",
21+
"language": "en",
22+
"category": [
23+
"regional"
24+
]
25+
},
26+
{
27+
"id": "example-project-001",
28+
"title": "Utility-scale battery project reaches commissioning",
29+
"description": "A fictional example describing project commissioning.",
30+
"url": "https://example.com/energy/project-commissioning",
31+
"published": "2026-07-24T10:30:00Z",
32+
"language": "en",
33+
"category": [
34+
"technology"
35+
]
36+
}
37+
],
38+
"page": 1
39+
}

0 commit comments

Comments
 (0)