Skip to content

Commit 2592cba

Browse files
committed
Harden auto-translation and language detection
1 parent aa56330 commit 2592cba

54 files changed

Lines changed: 4984 additions & 1473 deletions

Some content is hidden

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

apps/api/src/ai-pipeline/shared/tools/context.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ const requestKnowledgeClarificationMock = mock((async () => ({
4343
status: "awaiting_answer" as const,
4444
})) as (...args: unknown[]) => Promise<unknown>);
4545

46-
mock.module("@api/utils/vector-search", () => ({
46+
mock.module("@api/db/queries/vector-search", () => ({
4747
findSimilarKnowledge: findSimilarKnowledgeMock,
4848
}));
4949
mock.module("@api/db/queries/knowledge-clarification", () => ({

apps/api/src/ai-pipeline/shared/tools/context.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ import {
33
linkVisitorToContact,
44
updateContact,
55
} from "@api/db/queries/contact";
6+
import { findSimilarKnowledge } from "@api/db/queries/vector-search";
67
import { getCompleteVisitorWithContact } from "@api/db/queries/visitor";
78
import {
89
detectMessageLanguage,
910
didTranslationSucceed,
1011
maybeTranslateText,
1112
} from "@api/lib/translation";
12-
import { findSimilarKnowledge } from "@api/utils/vector-search";
1313
import { shouldTranslateBetweenLanguages } from "@cossistant/core";
1414
import { tool } from "ai";
1515
import { z } from "zod";

apps/api/src/db/queries/admin.ts

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
import type { Database } from "@api/db";
2+
import {
3+
member,
4+
organization,
5+
team,
6+
teamMember,
7+
type UserSelect,
8+
user,
9+
website,
10+
} from "@api/db/schema";
11+
import { WebsiteStatus } from "@cossistant/types";
12+
import { and, asc, desc, eq, ilike, inArray, isNull, or } from "drizzle-orm";
13+
14+
export async function listOrganizationPolarCustomerContactCandidates(
15+
db: Pick<Database, "select">,
16+
params: {
17+
organizationId: string;
18+
}
19+
): Promise<
20+
Array<{
21+
email: string;
22+
name: string | null;
23+
role: string | null;
24+
createdAt: string | Date | null;
25+
}>
26+
> {
27+
return db
28+
.select({
29+
email: user.email,
30+
name: user.name,
31+
role: member.role,
32+
createdAt: member.createdAt,
33+
})
34+
.from(member)
35+
.innerJoin(user, eq(member.userId, user.id))
36+
.where(eq(member.organizationId, params.organizationId))
37+
.orderBy(asc(member.createdAt));
38+
}
39+
40+
export async function listAdminUsers(
41+
db: Database,
42+
params: {
43+
search?: string | null;
44+
limit: number;
45+
}
46+
): Promise<UserSelect[]> {
47+
const likeTerm = params.search ? `%${params.search}%` : null;
48+
49+
return db
50+
.select()
51+
.from(user)
52+
.where(
53+
likeTerm
54+
? or(ilike(user.email, likeTerm), ilike(user.name, likeTerm))
55+
: undefined
56+
)
57+
.orderBy(desc(user.createdAt))
58+
.limit(params.limit);
59+
}
60+
61+
export type AdminWebsiteListRow = {
62+
id: string;
63+
name: string;
64+
slug: string;
65+
domain: string;
66+
logoUrl: string | null;
67+
status: WebsiteStatus;
68+
organizationId: string;
69+
organizationName: string;
70+
organizationSlug: string;
71+
teamId: string;
72+
createdAt: string;
73+
updatedAt: string;
74+
};
75+
76+
export async function listAdminWebsites(
77+
db: Database,
78+
params: {
79+
search?: string | null;
80+
limit: number;
81+
}
82+
): Promise<AdminWebsiteListRow[]> {
83+
const likeTerm = params.search ? `%${params.search}%` : null;
84+
85+
return db
86+
.select({
87+
id: website.id,
88+
name: website.name,
89+
slug: website.slug,
90+
domain: website.domain,
91+
logoUrl: website.logoUrl,
92+
status: website.status,
93+
organizationId: website.organizationId,
94+
organizationName: organization.name,
95+
organizationSlug: organization.slug,
96+
teamId: website.teamId,
97+
createdAt: website.createdAt,
98+
updatedAt: website.updatedAt,
99+
})
100+
.from(website)
101+
.innerJoin(organization, eq(website.organizationId, organization.id))
102+
.where(
103+
and(
104+
eq(website.status, WebsiteStatus.ACTIVE),
105+
isNull(website.deletedAt),
106+
likeTerm
107+
? or(
108+
ilike(website.name, likeTerm),
109+
ilike(website.slug, likeTerm),
110+
ilike(website.domain, likeTerm),
111+
ilike(organization.name, likeTerm)
112+
)
113+
: undefined
114+
)
115+
)
116+
.orderBy(desc(website.createdAt))
117+
.limit(params.limit);
118+
}
119+
120+
export async function getAdminUserById(
121+
db: Database,
122+
params: {
123+
userId: string;
124+
}
125+
): Promise<UserSelect | null> {
126+
const [selectedUser] = await db
127+
.select()
128+
.from(user)
129+
.where(eq(user.id, params.userId))
130+
.limit(1);
131+
132+
return selectedUser ?? null;
133+
}
134+
135+
export async function listAdminUserOrganizationMemberships(
136+
db: Database,
137+
params: {
138+
userId: string;
139+
}
140+
): Promise<
141+
Array<{
142+
organizationId: string;
143+
organizationName: string;
144+
organizationSlug: string;
145+
role: string | null;
146+
joinedAt: string | Date | null;
147+
}>
148+
> {
149+
return db
150+
.select({
151+
organizationId: organization.id,
152+
organizationName: organization.name,
153+
organizationSlug: organization.slug,
154+
role: member.role,
155+
joinedAt: member.createdAt,
156+
})
157+
.from(member)
158+
.innerJoin(organization, eq(member.organizationId, organization.id))
159+
.where(eq(member.userId, params.userId))
160+
.orderBy(desc(member.createdAt));
161+
}
162+
163+
export async function listAdminUserTeamMemberships(
164+
db: Database,
165+
params: {
166+
userId: string;
167+
}
168+
): Promise<Array<{ teamId: string; organizationId: string }>> {
169+
return db
170+
.select({
171+
teamId: teamMember.teamId,
172+
organizationId: team.organizationId,
173+
})
174+
.from(teamMember)
175+
.innerJoin(team, eq(teamMember.teamId, team.id))
176+
.where(eq(teamMember.userId, params.userId));
177+
}
178+
179+
export async function listAdminOrganizationsByIds(
180+
db: Database,
181+
params: {
182+
organizationIds: string[];
183+
}
184+
): Promise<Array<{ id: string; name: string; slug: string }>> {
185+
if (params.organizationIds.length === 0) {
186+
return [];
187+
}
188+
189+
return db
190+
.select({
191+
id: organization.id,
192+
name: organization.name,
193+
slug: organization.slug,
194+
})
195+
.from(organization)
196+
.where(inArray(organization.id, params.organizationIds));
197+
}
198+
199+
export async function listAdminWebsiteAccessRowsForOrganizations(
200+
db: Database,
201+
params: {
202+
organizationIds: string[];
203+
}
204+
): Promise<
205+
Array<{
206+
id: string;
207+
name: string;
208+
slug: string;
209+
domain: string;
210+
logoUrl: string | null;
211+
organizationId: string;
212+
teamId: string;
213+
createdAt: string;
214+
}>
215+
> {
216+
if (params.organizationIds.length === 0) {
217+
return [];
218+
}
219+
220+
return db
221+
.select({
222+
id: website.id,
223+
name: website.name,
224+
slug: website.slug,
225+
domain: website.domain,
226+
logoUrl: website.logoUrl,
227+
organizationId: website.organizationId,
228+
teamId: website.teamId,
229+
createdAt: website.createdAt,
230+
})
231+
.from(website)
232+
.where(
233+
and(
234+
inArray(website.organizationId, params.organizationIds),
235+
isNull(website.deletedAt)
236+
)
237+
)
238+
.orderBy(desc(website.createdAt));
239+
}
240+
241+
export async function getActiveAdminWebsiteById(
242+
db: Database,
243+
params: {
244+
websiteId: string;
245+
}
246+
): Promise<typeof website.$inferSelect | null> {
247+
const [site] = await db
248+
.select()
249+
.from(website)
250+
.where(
251+
and(
252+
eq(website.id, params.websiteId),
253+
eq(website.status, WebsiteStatus.ACTIVE),
254+
isNull(website.deletedAt)
255+
)
256+
)
257+
.limit(1);
258+
259+
return site ?? null;
260+
}
261+
262+
export async function getAdminWebsiteForAiUsageGrant(
263+
db: Database,
264+
params: {
265+
websiteId: string;
266+
}
267+
): Promise<{
268+
id: string;
269+
name: string;
270+
slug: string;
271+
organizationId: string;
272+
organizationName: string;
273+
} | null> {
274+
const [site] = await db
275+
.select({
276+
id: website.id,
277+
name: website.name,
278+
slug: website.slug,
279+
organizationId: website.organizationId,
280+
organizationName: organization.name,
281+
})
282+
.from(website)
283+
.innerJoin(organization, eq(website.organizationId, organization.id))
284+
.where(and(eq(website.id, params.websiteId), isNull(website.deletedAt)))
285+
.limit(1);
286+
287+
return site ?? null;
288+
}

apps/api/src/db/queries/ai-agent.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,28 @@ export async function getAiAgentForWebsite(
8080
return agent ?? null;
8181
}
8282

83+
export async function listActiveAiAgentSummariesForWebsite(
84+
db: Database,
85+
params: {
86+
websiteId: string;
87+
}
88+
): Promise<Array<{ id: string; name: string; image: string | null }>> {
89+
return db
90+
.select({
91+
id: aiAgent.id,
92+
name: aiAgent.name,
93+
image: aiAgent.image,
94+
})
95+
.from(aiAgent)
96+
.where(
97+
and(
98+
eq(aiAgent.websiteId, params.websiteId),
99+
eq(aiAgent.isActive, true),
100+
isNull(aiAgent.deletedAt)
101+
)
102+
);
103+
}
104+
83105
/**
84106
* Get a specific AI agent for a website by ID.
85107
* Returns null when the agent does not belong to the provided website/organization.

0 commit comments

Comments
 (0)