Use label names instead of IDs for thread label management (#1927)

# READ CAREFULLY THEN REMOVE

Remove bullet points that are not relevant.

PLEASE REFRAIN FROM USING AI TO WRITE YOUR CODE AND PR DESCRIPTION. IF YOU DO USE AI TO WRITE YOUR CODE PLEASE PROVIDE A DESCRIPTION AND REVIEW IT CAREFULLY. MAKE SURE YOU UNDERSTAND THE CODE YOU ARE SUBMITTING USING AI.

- Pull requests that do not follow these guidelines will be closed without review or comment.
- If you use AI to write your PR description your pr will be close without review or comment.
- If you are unsure about anything, feel free to ask for clarification.

## Description

Please provide a clear description of your changes.

---

## Type of Change

Please delete options that are not relevant.

- [ ] 🐛 Bug fix (non-breaking change which fixes an issue)
- [ ]  New feature (non-breaking change which adds functionality)
- [ ] 💥 Breaking change (fix or feature with breaking changes)
- [ ] 📝 Documentation update
- [ ] 🎨 UI/UX improvement
- [ ] 🔒 Security enhancement
- [ ]  Performance improvement

## Areas Affected

Please check all that apply:

- [ ] Email Integration (Gmail, IMAP, etc.)
- [ ] User Interface/Experience
- [ ] Authentication/Authorization
- [ ] Data Storage/Management
- [ ] API Endpoints
- [ ] Documentation
- [ ] Testing Infrastructure
- [ ] Development Workflow
- [ ] Deployment/Infrastructure

## Testing Done

Describe the tests you've done:

- [ ] Unit tests added/updated
- [ ] Integration tests added/updated
- [ ] Manual testing performed
- [ ] Cross-browser testing (if UI changes)
- [ ] Mobile responsiveness verified (if UI changes)

## Security Considerations

For changes involving data or authentication:

- [ ] No sensitive data is exposed
- [ ] Authentication checks are in place
- [ ] Input validation is implemented
- [ ] Rate limiting is considered (if applicable)

## Checklist

- [ ] I have read the [CONTRIBUTING](https://github.com/Mail-0/Zero/blob/staging/.github/CONTRIBUTING.md) document
- [ ] My code follows the project's style guidelines
- [ ] I have performed a self-review of my code
- [ ] I have commented my code, particularly in complex areas
- [ ] I have updated the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix/feature works
- [ ] All tests pass locally
- [ ] Any dependent changes are merged and published

## Additional Notes

Add any other context about the pull request here.

## Screenshots/Recordings

Add screenshots or recordings here if applicable.

---

_By submitting this pull request, I confirm that my contribution is made under the terms of the project's license._

    
<!-- This is an auto-generated description by cubic. -->
---

## Summary by cubic
Switched thread label management to use label names instead of label IDs throughout the backend.

- **Refactors**
  - Updated database schema and related code to remove label ID fields and use label names for adding, removing, and displaying thread labels.
  - Adjusted prompt instructions and tool descriptions to reference label names.

<!-- End of auto-generated description by cubic. -->



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

* **New Features**
  * Introduced a new web search tool that provides concise answers to user queries using Perplexity AI.

* **Bug Fixes**
  * Improved reliability when modifying thread labels, ensuring accurate label retrieval even if a thread is not initially found.

* **Refactor**
  * Updated label handling to use label names instead of IDs throughout the app.
  * Removed unused label-related data from thread records for improved data consistency.

* **Style**
  * Enhanced descriptions for label modification parameters to clarify expected input.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Adam
2025-08-05 11:11:59 -07:00
committed by GitHub
parent 0f7e2ed340
commit d2621c044b
6 changed files with 42 additions and 35 deletions

View File

@@ -394,7 +394,7 @@ export const AiChatPrompt = () =>
<tool name="${Tools.ModifyLabels}">
<purpose>Add/remove labels from threads</purpose>
<note>Get label IDs first with getUserLabels</note>
<note>Always use the label names, not the IDs</note>
<example>modifyLabels({ threadIds: [...], options: { addLabels: [...], removeLabels: [...] } })</example>
</tool>

View File

@@ -53,6 +53,7 @@ ${prompt}
const appendContext = (prompt: string, context?: Record<string, string>) => {
if (!context) return prompt;
return dedent`
<note>use sequential thinking to solve the user's problem</note>
<context>
<note>when the user asks about "this" thread or "this" email, use the threadId to get the thread details</note>
<note>when the user asks about "this" folder, use the currentFolder to get the folder details</note>

View File

@@ -20,7 +20,6 @@ const threadSelect = {
latestSender: threads.latestSender,
latestReceivedOn: threads.latestReceivedOn,
latestSubject: threads.latestSubject,
latestLabelIds: threads.latestLabelIds,
} as const;
async function createMissingLabels(db: DB, labelIds: string[]): Promise<void> {

View File

@@ -11,7 +11,6 @@ export const threads = sqliteTable(
latestSender: text('latest_sender', { mode: 'json' }).$type<Sender>(),
latestReceivedOn: text('latest_received_on'),
latestSubject: text('latest_subject'),
latestLabelIds: text('latest_label_ids', { mode: 'json' }).$type<string[]>(),
},
(table) => [
index('threads_thread_id_idx').on(table.threadId),

View File

@@ -34,7 +34,7 @@ import {
type ParsedMessage,
} from '../../types';
import type { IGetThreadResponse, IGetThreadsResponse, MailManager } from '../../lib/driver/types';
import { countThreads, countThreadsByLabel, create, get, modifyThreadLabels, type DB } from './db';
import { countThreads, countThreadsByLabel, create, get, getThreadLabels, modifyThreadLabels, type DB } from './db';
import { generateWhatUserCaresAbout, type UserTopic } from '../../lib/analyze/interests';
import { DurableObjectOAuthClientProvider } from 'agents/mcp/do-oauth-client-provider';
import { AiChatPrompt, GmailSearchAssistantSystemPrompt } from '../../lib/prompts';
@@ -60,6 +60,7 @@ import { openai } from '@ai-sdk/openai';
import * as schema from './db/schema';
import { threads } from './db/schema';
import { Effect, pipe } from 'effect';
import { groq } from '@ai-sdk/groq';
import { createDb } from '../../db';
import type { Message } from 'ai';
import { eq } from 'drizzle-orm';
@@ -952,17 +953,16 @@ export class ZeroDriver extends DurableObject<ZeroEnv> {
// Update database
yield* Effect.tryPromise(() =>
create(
this.db,
{
id: threadId,
threadId,
providerId: 'google',
latestSender: latest.sender,
latestReceivedOn: normalizedReceivedOn,
latestSubject: latest.subject,
latestLabelIds: latest.tags.map((tag) => tag.id),
},
latest.tags.map((tag) => tag.id),
this.db,
{
id: threadId,
threadId,
providerId: 'google',
latestSender: latest.sender,
latestReceivedOn: normalizedReceivedOn,
latestSubject: latest.subject,
},
latest.tags.map((tag) => tag.id),
),
).pipe(
Effect.tap(() =>
@@ -1610,25 +1610,20 @@ export class ZeroDriver extends DurableObject<ZeroEnv> {
async modifyThreadLabelsInDB(threadId: string, addLabels: string[], removeLabels: string[]) {
try {
// Get current labels before modification
const currentThread = await get(this.db, { id: threadId });
let currentThread = await get(this.db, { id: threadId });
if (!currentThread) {
throw new Error(`Thread ${threadId} not found in database`);
await this.syncThread({ threadId });
currentThread = await get(this.db, { id: threadId });
}
let currentLabels: string[];
try {
const labelIds = currentThread.latestLabelIds;
if (Array.isArray(labelIds)) {
currentLabels = labelIds;
} else {
currentLabels = [];
}
} catch (error) {
console.error(`Invalid JSON in latest_label_ids for thread ${threadId}:`, error);
currentLabels = [];
if (!currentThread) {
throw new Error(`Thread ${threadId} not found in database and could not be synced`);
}
const currentLabelsData = await getThreadLabels(this.db, threadId);
const currentLabels = currentLabelsData.map((l) => l.id);
// Use the new database operations to modify labels
const result = await modifyThreadLabels(this.db, threadId, addLabels, removeLabels);
@@ -1668,7 +1663,6 @@ export class ZeroDriver extends DurableObject<ZeroEnv> {
labels: [],
} satisfies IGetThreadResponse;
}
const row = result;
const storedThread = await this.env.THREADS_BUCKET.get(this.getThreadKey(id));
let messages: ParsedMessage[] = storedThread
@@ -1681,14 +1675,21 @@ export class ZeroDriver extends DurableObject<ZeroEnv> {
messages = messages.filter((e) => e.isDraft !== true);
}
const latestLabelIds = row.latestLabelIds;
const labelsList = await getThreadLabels(this.db, id);
const labelIds = labelsList.map((l) => l.id);
console.log(
'[getThreadFromDB] storedThread:',
labelIds,
messages.findLast((e) => e.isDraft !== true),
);
return {
messages,
latest: messages.findLast((e) => e.isDraft !== true),
hasUnread: latestLabelIds?.includes('UNREAD') || false,
hasUnread: labelIds.includes('UNREAD'),
totalReplies: messages.filter((e) => e.isDraft !== true).length,
labels: latestLabelIds?.map((id: string) => ({ id, name: id })) || [],
labels: labelsList,
isLatestDraft,
} satisfies IGetThreadResponse;
} catch (error) {
@@ -1813,7 +1814,7 @@ export class ZeroAgent extends AIChatAgent<ZeroEnv> {
const model =
this.env.USE_OPENAI === 'true'
? openai(this.env.OPENAI_MODEL || 'gpt-4o')
? groq('openai/gpt-oss-120b')
: anthropic(this.env.OPENAI_MODEL || 'claude-3-7-sonnet-20250219');
const result = streamText({

View File

@@ -242,8 +242,14 @@ const modifyLabels = (connectionId: string) =>
parameters: z.object({
threadIds: z.array(z.string()).describe('The IDs of the threads to modify'),
options: z.object({
addLabels: z.array(z.string()).default([]).describe('The labels to add'),
removeLabels: z.array(z.string()).default([]).describe('The labels to remove'),
addLabels: z
.array(z.string())
.default([])
.describe('The labels to add, an array of label names'),
removeLabels: z
.array(z.string())
.default([])
.describe('The labels to remove, an array of label names'),
}),
}),
execute: async ({ threadIds, options }) => {
@@ -482,6 +488,7 @@ export const tools = async (connectionId: string, ragEffect: boolean = false) =>
[Tools.DeleteLabel]: deleteLabel(connectionId),
[Tools.BuildGmailSearchQuery]: buildGmailSearchQuery(),
[Tools.GetCurrentDate]: getCurrentDate(),
[Tools.WebSearch]: webSearch(),
[Tools.InboxRag]: tool({
description:
'Search the inbox for emails using natural language. Returns only an array of threadIds.',