Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions js/app/packages/block-md/component/TaskDuplicateList.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { QUERY_FILTERS_BASE } from '@app/component/next-soup/filters/query-filters';
import { TaskListEntity } from '@app/component/next-soup/soup-view/views/tasks/TaskListEntity';
import { useFeatureFlag } from '@app/lib/analytics/posthog';
import { ENABLE_TASK_DUPLICATES_FLAG } from '@core/constant/featureFlags';
import { useTaskDedupFlag } from '@core/constant/featureFlags';
import { ListLayoutProvider } from '@entity';
import CaretRightIcon from '@phosphor/caret-right.svg';
import CopyIcon from '@phosphor/copy.svg';
Expand Down Expand Up @@ -124,7 +123,7 @@ export function SimilarTasksSection(props: {
content: Accessor<string>;
onOpenTask: (taskId: string) => void;
}) {
const flag = useFeatureFlag(ENABLE_TASK_DUPLICATES_FLAG);
const flag = useTaskDedupFlag();

const [debounced, setDebounced] = createSignal<DebouncedInput>({
title: props.title(),
Expand Down
15 changes: 5 additions & 10 deletions js/app/packages/block-md/component/TaskDuplicateMatches.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ import { useFeatureFlag } from '@app/lib/analytics/posthog';
import { useBlockId } from '@core/block';
import { DocumentMention } from '@core/component/LexicalMarkdown/component/decorator/DocumentMention';
import { toast } from '@core/component/Toast/Toast';
import {
ENABLE_TASK_DUPLICATES_FLAG,
ENABLE_TASK_DUPLICATES_OVERRIDE,
} from '@core/constant/featureFlags';
import { useTaskDedupFlag } from '@core/constant/featureFlags';
import CaretDownIcon from '@phosphor/caret-down.svg';
import WarningIcon from '@phosphor/warning.svg';
import {
Expand All @@ -18,9 +15,8 @@ import { Button, cn, Dropdown } from '@ui';
import { createMemo, createSignal, For, Show, Suspense } from 'solid-js';

export function TaskDuplicateMatchPill() {
const flag = useFeatureFlag(ENABLE_TASK_DUPLICATES_FLAG, {
enabledOverride: ENABLE_TASK_DUPLICATES_OVERRIDE,
});
const flag = useTaskDedupFlag();

const matches = useTaskDuplicateMatches();
const [open, setOpen] = createSignal(false);

Expand Down Expand Up @@ -58,9 +54,8 @@ export function TaskDuplicateMatchPill() {
}

export function TaskDuplicateMatchesSidePanelSection() {
const flag = useFeatureFlag(ENABLE_TASK_DUPLICATES_FLAG, {
enabledOverride: ENABLE_TASK_DUPLICATES_OVERRIDE,
});
const flag = useTaskDedupFlag();

const matches = useTaskDuplicateMatches();

return (
Expand Down
10 changes: 8 additions & 2 deletions js/app/packages/core/constant/featureFlags.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { analytics } from '@app/lib/analytics';
import { useFeatureFlag } from '@app/lib/analytics/posthog';

/**
* This constant reflects whether the app is running locally with hot reload enabled
Expand Down Expand Up @@ -362,8 +363,13 @@ export const ENABLE_TEAM_INVITE_TIERS_OVERRIDE = DEV_MODE_ENV

export const ENABLE_SOUP_GROUP_BY_OVERRIDE = DEV_MODE_ENV ? true : undefined;

export const ENABLE_TASK_DUPLICATES_FLAG = 'enable-task-duplicates';
export const ENABLE_TASK_DUPLICATES_OVERRIDE = DEV_MODE_ENV ? true : undefined;
const ENABLE_TASK_DUPLICATES_FLAG = 'enable-task-duplicates';
const ENABLE_TASK_DUPLICATES_OVERRIDE = DEV_MODE_ENV ? true : undefined;

export const useTaskDedupFlag = () =>
useFeatureFlag(ENABLE_TASK_DUPLICATES_FLAG, {
enabledOverride: ENABLE_TASK_DUPLICATES_OVERRIDE,
});

// Snippets: reusable markdown documents, the `c` launcher entry, and the `;`
// insert menu. PostHog-gated (currently targeted at the Macro team) with a
Expand Down
1 change: 1 addition & 0 deletions js/app/packages/queries/storage/task-duplicates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ async function searchSimilarTasks(
export function useTaskSimilaritySearchQuery(
input: Accessor<TaskSimilaritySearchInput>
) {
console.log('USER TAKS DEDUP');
return useQuery(() => ({
queryKey: taskSimilaritySearchKeys.forInput(input()).queryKey,
queryFn: () => searchSimilarTasks(input()),
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions rust/cloud-storage/task_dedup/src/domain/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ pub struct TaskSearchParameters {
/// When true, drop candidates already dismissed against
/// [`exclude_document_id`](Self::exclude_document_id).
pub exclude_dismissed: bool,
/// When true, only return incomplete tasks: candidates whose Status
/// property is Completed or Canceled are dropped (tasks without a status
/// are kept).
pub only_incomplete: bool,
}

/// A duplicate task candidate shown on the task surface.
Expand Down
6 changes: 6 additions & 0 deletions rust/cloud-storage/task_dedup/src/domain/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,9 @@ where
limit: self.config.vector_candidate_limit,
exclude_document_id: None,
exclude_dismissed: false,
// The composer shows these as actionable duplicates, so tasks that
// are already completed or canceled are not useful matches.
only_incomplete: true,
};
let results = self
.vector_db
Expand Down Expand Up @@ -263,6 +266,9 @@ where
limit: self.config.vector_candidate_limit,
exclude_document_id: Some(task.document_id.clone()),
exclude_dismissed: true,
// A persisted match against a completed task is still informative
// on the task surface (the work may already be done).
only_incomplete: false,
};
let results = self
.vector_db
Expand Down
5 changes: 4 additions & 1 deletion rust/cloud-storage/task_dedup/src/domain/service/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,7 @@ async fn detect_creates_match_reranks_and_notifies() {
.unwrap();
assert_eq!(params.exclude_document_id.as_deref(), Some("NEW"));
assert!(params.exclude_dismissed);
assert!(!params.only_incomplete);

// A single ordered match was written, tagged with the judge's model.
let upserts = h.matches.upserted_matches.lock().unwrap();
Expand Down Expand Up @@ -706,7 +707,8 @@ async fn similarity_search_filters_ranks_and_persists_nothing() {
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].1, vec!["cx".to_string(), "cz".to_string()]);

// The similarity path does not scope-exclude self or dismissed pairs.
// The similarity path does not scope-exclude self or dismissed pairs, but
// it does restrict candidates to incomplete tasks.
let params = h
.vector_db
.inner
Expand All @@ -717,6 +719,7 @@ async fn similarity_search_filters_ranks_and_persists_nothing() {
.unwrap();
assert!(params.exclude_document_id.is_none());
assert!(!params.exclude_dismissed);
assert!(params.only_incomplete);

// Nothing was judged or persisted.
assert!(h.judge.calls.lock().unwrap().is_empty());
Expand Down
19 changes: 19 additions & 0 deletions rust/cloud-storage/task_dedup/src/outbound/postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,24 @@ impl VectorStore<DIMS> for PgTaskVectorDb {
AND m.status = 'dismissed'
)
)
-- only_incomplete: drop tasks whose Status system property
-- (system_properties::SystemPropertyKey::Status) is set to
-- the Completed or Canceled option. Tasks without a status
-- row are kept.
AND (
NOT $8
OR NOT EXISTS (
SELECT 1
FROM entity_properties ep
WHERE ep.entity_id = e.document_id
AND ep.entity_type = 'TASK'
AND ep.property_definition_id = '00000001-0000-0000-0000-000000000002'
AND ep.values->'value' ?| ARRAY[
'00000001-0000-0000-0002-000000000004',
'00000001-0000-0000-0002-000000000005'
]
)
)
GROUP BY e.document_id, e.search_key, e.content, e.embedding
),
ranked AS (
Expand All @@ -176,6 +194,7 @@ impl VectorStore<DIMS> for PgTaskVectorDb {
params.exclude_document_id,
params.exclude_dismissed,
params.limit,
params.only_incomplete,
)
.fetch_all(&mut *tx)
.await?;
Expand Down
89 changes: 89 additions & 0 deletions rust/cloud-storage/task_dedup/src/outbound/postgres/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ const TEAM_ID: Uuid = uuid::uuid!("a0000000-0000-0000-0000-000000000001");
const TASK_ONE: &str = "d1000000-0000-0000-0000-000000000001";
const TASK_TWO: &str = "d1000000-0000-0000-0000-000000000002";
const TASK_THREE: &str = "d1000000-0000-0000-0000-000000000003";
const TASK_FOUR: &str = "d1000000-0000-0000-0000-000000000004";

// Status system property options (see `system_properties::StatusOption`).
const STATUS_IN_PROGRESS: Uuid = uuid::uuid!("00000001-0000-0000-0002-000000000002");
const STATUS_COMPLETED: Uuid = uuid::uuid!("00000001-0000-0000-0002-000000000004");
const STATUS_CANCELED: Uuid = uuid::uuid!("00000001-0000-0000-0002-000000000005");

type TestService = TaskDedupService<DIMS, LocalEmbedder, PgTaskVectorDb, NoOpReranker>;

Expand Down Expand Up @@ -157,6 +163,31 @@ async fn insert_task_embedding(pool: &PgPool, document_id: &str, title: &str, bo
}
}

/// Sets a task's Status system property to the given option uuid (e.g.
/// `system_properties` Completed `…0002-000000000004`).
async fn set_task_status(pool: &PgPool, document_id: &str, status_option: Uuid) {
sqlx::query!(
r#"
INSERT INTO entity_properties (id, entity_id, entity_type, property_definition_id, values)
VALUES (
$1,
$2,
'TASK',
'00000001-0000-0000-0000-000000000002',
jsonb_build_object('type', 'SelectOption', 'value', jsonb_build_array($3::text))
)
ON CONFLICT (entity_id, entity_type, property_definition_id) DO UPDATE
SET values = EXCLUDED.values
"#,
Uuid::new_v4(),
document_id,
status_option.to_string(),
)
.execute(pool)
.await
.unwrap();
}

async fn insert_match(pool: &PgPool, task_id: &str, duplicate_task_id: &str) -> Uuid {
let id = Uuid::new_v4();
let (task_id, duplicate_task_id) =
Expand Down Expand Up @@ -450,6 +481,64 @@ async fn detection_closes_existing_duplicate_component(pool: PgPool) {
assert_eq!(duplicate_ids, vec![TASK_TWO, TASK_THREE]);
}

#[sqlx::test(
migrator = "MACRO_DB_MIGRATIONS",
fixtures(
path = "../../../../documents/fixtures",
scripts("documents_test_data")
)
)]
async fn similarity_search_only_returns_incomplete_tasks(pool: PgPool) {
setup_tasks(&pool).await;
insert_task(&pool, TASK_FOUR, "Detect duplicated tasks", OWNER).await;
for task in [TASK_ONE, TASK_TWO, TASK_THREE, TASK_FOUR] {
insert_task_embedding(&pool, task, DETECTION_TITLE, DETECTION_BODY).await;
}
set_task_status(&pool, TASK_ONE, STATUS_COMPLETED).await;
set_task_status(&pool, TASK_TWO, STATUS_CANCELED).await;
set_task_status(&pool, TASK_THREE, STATUS_IN_PROGRESS).await;
// TASK_FOUR has no status row and counts as incomplete.

let service = service(pool.clone());
let results = service
.similarity_search(OWNER, Some(TEAM_ID), DETECTION_TITLE, DETECTION_BODY)
.await
.unwrap();

let mut ids: Vec<&str> = results.iter().map(|r| r.task_id.as_str()).collect();
ids.sort();
assert_eq!(
ids,
vec![TASK_THREE, TASK_FOUR],
"completed and canceled tasks should be dropped; in-progress and status-less kept"
);
}

#[sqlx::test(
migrator = "MACRO_DB_MIGRATIONS",
fixtures(
path = "../../../../documents/fixtures",
scripts("documents_test_data")
)
)]
async fn detection_still_matches_completed_tasks(pool: PgPool) {
setup_tasks(&pool).await;
insert_task_embedding(&pool, TASK_TWO, DETECTION_TITLE, DETECTION_BODY).await;
set_task_status(&pool, TASK_TWO, STATUS_COMPLETED).await;

let service = service(pool.clone());
service
.detect_new_task(detection_task(TASK_ONE))
.await
.unwrap();

// The new-task path keeps completed candidates: a match against finished
// work is still worth surfacing on the task page.
let duplicates = service.active_duplicates(TASK_ONE).await.unwrap();
assert_eq!(duplicates.len(), 1);
assert_eq!(duplicates[0].task_id, TASK_TWO);
}

#[sqlx::test(
migrator = "MACRO_DB_MIGRATIONS",
fixtures(
Expand Down
Loading