Skip to content

Commit 789c864

Browse files
Chat various improvements (#3942)
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent d698003 commit 789c864

File tree

112 files changed

+2551
-1375
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

112 files changed

+2551
-1375
lines changed

Cargo.lock

Lines changed: 31 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ hypr-host = { path = "crates/host", package = "host" }
6666
hypr-http = { path = "crates/http", package = "hypr-http-utils" }
6767
hypr-importer-core = { path = "crates/importer-core", package = "importer-core" }
6868
hypr-intercept = { path = "crates/intercept", package = "intercept" }
69+
hypr-jina = { path = "crates/jina", package = "jina" }
6970
hypr-kyutai = { path = "crates/kyutai", package = "kyutai" }
7071
hypr-language = { path = "crates/language", package = "language" }
7172
hypr-llama = { path = "crates/llama", package = "llama" }
@@ -98,6 +99,7 @@ hypr-tcc = { path = "crates/tcc", package = "tcc" }
9899
hypr-template-app = { path = "crates/template-app", package = "template-app" }
99100
hypr-template-app-legacy = { path = "crates/template-app-legacy", package = "template-app-legacy" }
100101
hypr-template-eval = { path = "crates/template-eval", package = "template-eval" }
102+
hypr-template-support = { path = "crates/template-support", package = "template-support" }
101103
hypr-tiptap = { path = "crates/tiptap", package = "tiptap" }
102104
hypr-transcribe-aws = { path = "crates/transcribe-aws", package = "transcribe-aws" }
103105
hypr-transcribe-azure = { path = "crates/transcribe-azure", package = "transcribe-azure" }

apps/api/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ hypr-api-auth = { workspace = true }
99
hypr-api-calendar = { workspace = true }
1010
hypr-api-env = { workspace = true }
1111
hypr-api-nango = { workspace = true }
12+
hypr-api-research = { workspace = true }
1213
hypr-api-subscription = { workspace = true }
1314
hypr-api-support = { workspace = true }
1415
hypr-llm-proxy = { workspace = true }

apps/api/openapi.gen.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,18 @@
466466
"arch": {
467467
"type": "string"
468468
},
469+
"buildHash": {
470+
"type": [
471+
"string",
472+
"null"
473+
]
474+
},
475+
"locale": {
476+
"type": [
477+
"string",
478+
"null"
479+
]
480+
},
469481
"osVersion": {
470482
"type": "string"
471483
},

apps/api/src/env.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ pub struct Env {
2727
#[serde(flatten)]
2828
pub support_database: hypr_api_support::SupportDatabaseEnv,
2929

30+
pub exa_api_key: String,
31+
pub jina_api_key: String,
32+
3033
#[serde(flatten)]
3134
pub llm: hypr_llm_proxy::Env,
3235
#[serde(flatten)]

apps/api/src/main.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ async fn app() -> Router {
5454
&env.supabase,
5555
auth_state_support.clone(),
5656
);
57+
let research_config = hypr_api_research::ResearchConfig {
58+
exa_api_key: env.exa_api_key.clone(),
59+
jina_api_key: env.jina_api_key.clone(),
60+
};
5761

5862
let webhook_routes = Router::new().nest(
5963
"/nango",
@@ -63,6 +67,7 @@ async fn app() -> Router {
6367
let pro_routes = Router::new()
6468
.merge(hypr_transcribe_proxy::listen_router(stt_config.clone()))
6569
.merge(hypr_llm_proxy::chat_completions_router(llm_config.clone()))
70+
.merge(hypr_api_research::router(research_config))
6671
.nest("/stt", hypr_transcribe_proxy::router(stt_config))
6772
.nest("/llm", hypr_llm_proxy::router(llm_config))
6873
.nest("/calendar", hypr_api_calendar::router(calendar_config))
Lines changed: 110 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,112 @@
1-
export type ContextItem = {
2-
key: string;
3-
label: string;
4-
tooltip: string;
1+
import type { AccountInfo } from "@hypr/plugin-auth";
2+
import type { DeviceInfo } from "@hypr/plugin-misc";
3+
import type { ChatContext } from "@hypr/plugin-template";
4+
5+
import type { HyprUIMessage } from "./types";
6+
import { isRecord } from "./utils";
7+
8+
export type ContextEntity =
9+
| {
10+
kind: "session";
11+
key: string;
12+
chatContext: ChatContext;
13+
wordCount?: number;
14+
rawNotePreview?: string;
15+
participantCount?: number;
16+
eventTitle?: string;
17+
removable?: boolean;
18+
}
19+
| ({ kind: "account"; key: string } & Partial<AccountInfo>)
20+
| ({
21+
kind: "device";
22+
key: string;
23+
} & Partial<DeviceInfo>);
24+
25+
export type ContextEntityKind = ContextEntity["kind"];
26+
27+
type ToolOutputAvailablePart = {
28+
type: string;
29+
state: "output-available";
30+
output?: unknown;
31+
};
32+
33+
function isToolOutputAvailablePart(
34+
value: unknown,
35+
): value is ToolOutputAvailablePart {
36+
return (
37+
isRecord(value) &&
38+
typeof value.type === "string" &&
39+
value.state === "output-available"
40+
);
41+
}
42+
43+
function parseSearchSessionsOutput(output: unknown): ContextEntity[] {
44+
if (!isRecord(output) || !Array.isArray(output.results)) {
45+
return [];
46+
}
47+
48+
return output.results.flatMap((item): ContextEntity[] => {
49+
if (!isRecord(item)) {
50+
return [];
51+
}
52+
53+
if (typeof item.id !== "string" && typeof item.id !== "number") {
54+
return [];
55+
}
56+
57+
const title = typeof item.title === "string" ? item.title : null;
58+
const content = typeof item.content === "string" ? item.content : null;
59+
60+
return [
61+
{
62+
kind: "session",
63+
key: `session:search:${item.id}`,
64+
chatContext: {
65+
title,
66+
date: null,
67+
rawContent: content,
68+
enhancedContent: null,
69+
transcript: null,
70+
},
71+
rawNotePreview: content?.slice(0, 120) ?? undefined,
72+
removable: true,
73+
},
74+
];
75+
});
76+
}
77+
78+
const toolEntityExtractors: Record<
79+
string,
80+
(output: unknown) => ContextEntity[]
81+
> = {
82+
search_sessions: parseSearchSessionsOutput,
583
};
684

7-
export type ContextSource =
8-
| { type: "account"; email?: string; userId?: string }
9-
| { type: "device" }
10-
| { type: "session"; title?: string; date?: string }
11-
| { type: "transcript"; wordCount?: number }
12-
| { type: "note"; preview?: string };
85+
export function extractToolContextEntities(
86+
messages: Array<Pick<HyprUIMessage, "parts">>,
87+
): ContextEntity[] {
88+
const seen = new Set<string>();
89+
const entities: ContextEntity[] = [];
90+
91+
for (const message of messages) {
92+
if (!Array.isArray(message.parts)) continue;
93+
for (const part of message.parts) {
94+
if (!isToolOutputAvailablePart(part) || !part.type.startsWith("tool-")) {
95+
continue;
96+
}
97+
98+
const toolName = part.type.slice(5);
99+
const extractor = toolEntityExtractors[toolName];
100+
if (!extractor) continue;
101+
102+
for (const entity of extractor(part.output)) {
103+
if (!seen.has(entity.key)) {
104+
seen.add(entity.key);
105+
entities.push(entity);
106+
}
107+
}
108+
}
109+
}
110+
111+
return entities;
112+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { ContextEntity } from "../context-item";
2+
3+
export function composeContextEntities(
4+
groups: ContextEntity[][],
5+
): ContextEntity[] {
6+
const seen = new Set<string>();
7+
const merged: ContextEntity[] = [];
8+
9+
for (const group of groups) {
10+
for (const entity of group) {
11+
if (seen.has(entity.key)) continue;
12+
seen.add(entity.key);
13+
merged.push(entity);
14+
}
15+
}
16+
17+
return merged;
18+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { commands as miscCommands } from "@hypr/plugin-misc";
2+
3+
import type { ContextEntity } from "../context-item";
4+
5+
export async function collectDeviceEntity(): Promise<
6+
Extract<ContextEntity, { kind: "device" }>
7+
> {
8+
let deviceContext: Extract<ContextEntity, { kind: "device" }> = {
9+
kind: "device",
10+
key: "support:device",
11+
};
12+
13+
try {
14+
const deviceContextResult = await miscCommands.getDeviceInfo(
15+
navigator.language || "en",
16+
);
17+
if (deviceContextResult.status === "ok") {
18+
deviceContext = {
19+
...deviceContext,
20+
...deviceContextResult.data,
21+
};
22+
}
23+
} catch (error) {
24+
console.error("Failed to collect device context:", error);
25+
}
26+
27+
return deviceContext;
28+
}

0 commit comments

Comments
 (0)