///|
struct Options {
limit : Int
comments : Int
lang : String
model : String
raw : Bool
}
///|
struct DeepSeekMessage {
content : String
} derive(FromJson, @debug.Debug)
///|
struct DeepSeekChoice {
message : DeepSeekMessage
} derive(FromJson, @debug.Debug)
///|
struct DeepSeekResponse {
choices : Array[DeepSeekChoice]
} derive(FromJson, @debug.Debug)
///|
async fn fetch_json(url : String) -> Json {
let (response, body) = @http.get(url)
guard response.code == 200 else {
fail("GET \{url} returned HTTP \{response.code}")
}
body.json()
}
///|
async fn fetch_item(id : Int) -> HnItem {
let json = fetch_json("https://hacker-news.firebaseio.com/v0/item/\{id}.json")
@json.from_json(json)
}
///|
async fn fetch_top_story_ids() -> Array[Int] {
let json = fetch_json("https://hacker-news.firebaseio.com/v0/topstories.json")
@json.from_json(json)
}
///|
async fn fetch_hot_comments(
story : HnItem,
max_comments : Int,
) -> Array[CommentBrief] {
match story.kids {
None => []
Some(kids) => {
let limit = max_comments.min(kids.length())
@async.with_task_group() <| group => {
let tasks = kids[:limit].map(id => group.spawn(() => fetch_item(id)))
let comments : Array[CommentBrief] = []
for task in tasks {
let item = task.wait()
if item.is_visible() && item.text != None {
comments.push(item.to_comment_brief())
}
}
comments
}
}
}
}
///|
async fn fetch_brief(options : Options) -> Array[StoryBrief] {
let ids = fetch_top_story_ids()
let limit = options.limit.min(ids.length())
@async.with_task_group() <| group => {
let tasks = ids[:limit].map(id => {
group.spawn(() => {
let item = fetch_item(id)
if item.is_visible() && item.title != None {
let comments = fetch_hot_comments(item, options.comments)
Some(item.to_story_brief(comments))
} else {
None
}
})
})
let stories : Array[StoryBrief] = []
for task in tasks {
match task.wait() {
Some(story) => stories.push(story)
None => ()
}
}
stories
}
}
///|
async fn call_deepseek(
api_key : String,
model : String,
prompt : String,
) -> String {
let headers = {
"Content-Type": "application/json",
"Authorization": "Bearer \{api_key}",
}
let request = (
{
"model": model,
"messages": [
{
"role": "system",
"content": "You produce concise Hacker News technical briefings.",
},
{ "role": "user", "content": prompt },
],
"stream": false,
} : Json).stringify()
let (response, body) = @http.post(
"https://api.deepseek.com/chat/completions",
request,
headers~,
)
let text = body.text()
guard response.code == 200 else {
fail("DeepSeek returned HTTP \{response.code}: \{text}")
}
let parsed : DeepSeekResponse = @json.from_json(@json.parse(text))
guard parsed.choices is [first, ..] else {
fail("DeepSeek returned no choices")
}
first.message.content
}
///|
async fn run_brief(options : Options) -> String {
let stories = fetch_brief(options)
guard @env.get_env_var("DEEPSEEK_API_KEY") is Some(api_key) else {
fail("DEEPSEEK_API_KEY is required.")
}
let summary = call_deepseek(
api_key,
options.model,
summary_prompt(stories, options.lang),
)
render_markdown(
stories,
summary,
model_label=options.model,
show_context=options.raw,
)
}
///|
fn read_options() -> Options raise {
let matches = @argparse.Command(
"hn-brief",
about=(
#|Fetch Hacker News top stories, ranked comments, and summarize them with DeepSeek.
),
flags=[
FlagArg(
"raw",
about="include the HN stories and comments sent to DeepSeek",
),
],
options=[
OptionArg("limit", about="number of HN stories to fetch"),
OptionArg("comments", about="ranked comments to fetch per story"),
OptionArg("lang", about="summary language label"),
OptionArg("model", about="DeepSeek model name"),
],
).parse()
{
limit: match matches.values {
{ "limit": [value], .. } => @string.parse_int(value)
_ => 10
},
comments: match matches.values {
{ "comments": [value], .. } => @string.parse_int(value)
_ => 3
},
lang: match matches.values {
{ "lang": [value], .. } => value
_ => "Simplified Chinese"
},
model: match matches.values {
{ "model": [value], .. } => value
_ => "deepseek-v4-flash"
},
raw: match matches.flags {
{ "raw": true, .. } => true
_ => false
},
}
}
///|
async fn main {
let options = read_options()
println(run_brief(options))
}