From 58d9aa4702a23f968919c56acecff7e529f0883b Mon Sep 17 00:00:00 2001 From: Sameer Pashikanti Date: Wed, 26 Nov 2025 14:34:36 +0000 Subject: [PATCH 1/5] fix chat context --- echo/.gitignore | 4 +- .../sync/collections/permissions.json | 35 ++++++++++-- .../project_chat_message_metadata.json | 28 ---------- .../project_chat/used_conversations.json | 2 +- .../project_chat_id.json | 7 ++- .../chat_message_metadata.json | 28 ---------- .../conversation.json | 48 ---------------- .../date_created.json | 48 ---------------- .../project_chat_message_metadata/id.json | 46 ---------------- .../message_metadata.json | 44 --------------- .../project_chat_message_metadata/ratio.json | 44 --------------- .../reference_text.json | 44 --------------- .../project_chat_message_metadata/type.json | 55 ------------------- .../conversation.json | 25 --------- .../message_metadata.json | 25 --------- echo/server/dembrane/service/chat.py | 11 ++-- 16 files changed, 44 insertions(+), 450 deletions(-) delete mode 100644 echo/directus/sync/snapshot/collections/project_chat_message_metadata.json delete mode 100644 echo/directus/sync/snapshot/fields/project_chat_message/chat_message_metadata.json delete mode 100644 echo/directus/sync/snapshot/fields/project_chat_message_metadata/conversation.json delete mode 100644 echo/directus/sync/snapshot/fields/project_chat_message_metadata/date_created.json delete mode 100644 echo/directus/sync/snapshot/fields/project_chat_message_metadata/id.json delete mode 100644 echo/directus/sync/snapshot/fields/project_chat_message_metadata/message_metadata.json delete mode 100644 echo/directus/sync/snapshot/fields/project_chat_message_metadata/ratio.json delete mode 100644 echo/directus/sync/snapshot/fields/project_chat_message_metadata/reference_text.json delete mode 100644 echo/directus/sync/snapshot/fields/project_chat_message_metadata/type.json delete mode 100644 echo/directus/sync/snapshot/relations/project_chat_message_metadata/conversation.json delete mode 100644 echo/directus/sync/snapshot/relations/project_chat_message_metadata/message_metadata.json diff --git a/echo/.gitignore b/echo/.gitignore index dc5fd8b1..04258b7a 100644 --- a/echo/.gitignore +++ b/echo/.gitignore @@ -24,4 +24,6 @@ __queuestorage__echo/server/dembrane/workspace_script.py .pytest_cache .DS_Store -.env.backup* \ No newline at end of file +.env.backup* + +tools \ No newline at end of file diff --git a/echo/directus/sync/collections/permissions.json b/echo/directus/sync/collections/permissions.json index 40b7a65f..80a3a4f7 100644 --- a/echo/directus/sync/collections/permissions.json +++ b/echo/directus/sync/collections/permissions.json @@ -1070,11 +1070,7 @@ "_and": [ { "project_chat_id": { - "project_id": { - "directus_user_id": { - "_eq": "$CURRENT_USER_ID" - } - } + "_nnull": true } } ] @@ -2185,5 +2181,34 @@ ], "policy": "abf8a154-5b1c-4a46-ac9c-7300570f4f17", "_syncId": "8a8e0d56-e394-47af-8473-9c77b6a0870f" + }, + { + "collection": "project", + "action": "read", + "permissions": null, + "validation": null, + "presets": null, + "fields": [ + "id", + "created_at", + "updated_at", + "language", + "conversation_ask_for_participant_name_label", + "default_conversation_ask_for_participant_name", + "default_conversation_tutorial_slug", + "default_conversation_title", + "default_conversation_description", + "default_conversation_finish_text", + "tags", + "is_conversation_allowed", + "is_get_reply_enabled", + "is_project_notification_subscription_allowed", + "is_verify_enabled", + "custom_verification_topics", + "selected_verification_key_list", + "is_enhanced_audio_processing_enabled" + ], + "policy": "abf8a154-5b1c-4a46-ac9c-7300570f4f17", + "_syncId": "f7ef008d-dbf6-4af1-8c66-97711f0ccb3f" } ] diff --git a/echo/directus/sync/snapshot/collections/project_chat_message_metadata.json b/echo/directus/sync/snapshot/collections/project_chat_message_metadata.json deleted file mode 100644 index 1c6013aa..00000000 --- a/echo/directus/sync/snapshot/collections/project_chat_message_metadata.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "collection": "project_chat_message_metadata", - "meta": { - "accountability": "all", - "archive_app_filter": true, - "archive_field": null, - "archive_value": null, - "collapse": "open", - "collection": "project_chat_message_metadata", - "color": null, - "display_template": null, - "group": "project_chat_message", - "hidden": true, - "icon": null, - "item_duplication_fields": null, - "note": null, - "preview_url": null, - "singleton": false, - "sort": 1, - "sort_field": null, - "translations": null, - "unarchive_value": null, - "versioning": false - }, - "schema": { - "name": "project_chat_message_metadata" - } -} diff --git a/echo/directus/sync/snapshot/fields/project_chat/used_conversations.json b/echo/directus/sync/snapshot/fields/project_chat/used_conversations.json index ea824f8f..c6e45304 100644 --- a/echo/directus/sync/snapshot/fields/project_chat/used_conversations.json +++ b/echo/directus/sync/snapshot/fields/project_chat/used_conversations.json @@ -10,7 +10,7 @@ "field": "used_conversations", "group": null, "hidden": false, - "interface": "list-m2m", + "interface": null, "note": null, "options": null, "readonly": false, diff --git a/echo/directus/sync/snapshot/fields/project_chat_conversation/project_chat_id.json b/echo/directus/sync/snapshot/fields/project_chat_conversation/project_chat_id.json index c1576d55..045a0514 100644 --- a/echo/directus/sync/snapshot/fields/project_chat_conversation/project_chat_id.json +++ b/echo/directus/sync/snapshot/fields/project_chat_conversation/project_chat_id.json @@ -10,9 +10,12 @@ "field": "project_chat_id", "group": null, "hidden": false, - "interface": null, + "interface": "select-dropdown-m2o", "note": null, - "options": null, + "options": { + "enableLink": true, + "template": "{{project_id.name}}" + }, "readonly": false, "required": false, "searchable": true, diff --git a/echo/directus/sync/snapshot/fields/project_chat_message/chat_message_metadata.json b/echo/directus/sync/snapshot/fields/project_chat_message/chat_message_metadata.json deleted file mode 100644 index 968c0259..00000000 --- a/echo/directus/sync/snapshot/fields/project_chat_message/chat_message_metadata.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "collection": "project_chat_message", - "field": "chat_message_metadata", - "type": "alias", - "meta": { - "collection": "project_chat_message", - "conditions": null, - "display": null, - "display_options": null, - "field": "chat_message_metadata", - "group": null, - "hidden": false, - "interface": "list-o2m", - "note": null, - "options": null, - "readonly": false, - "required": false, - "searchable": true, - "sort": 11, - "special": [ - "o2m" - ], - "translations": null, - "validation": null, - "validation_message": null, - "width": "full" - } -} diff --git a/echo/directus/sync/snapshot/fields/project_chat_message_metadata/conversation.json b/echo/directus/sync/snapshot/fields/project_chat_message_metadata/conversation.json deleted file mode 100644 index e65405db..00000000 --- a/echo/directus/sync/snapshot/fields/project_chat_message_metadata/conversation.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "collection": "project_chat_message_metadata", - "field": "conversation", - "type": "uuid", - "meta": { - "collection": "project_chat_message_metadata", - "conditions": null, - "display": null, - "display_options": null, - "field": "conversation", - "group": null, - "hidden": false, - "interface": "select-dropdown-m2o", - "note": null, - "options": { - "template": "{{id}} . {{participant_name}}" - }, - "readonly": false, - "required": false, - "searchable": true, - "sort": 7, - "special": [ - "m2o" - ], - "translations": null, - "validation": null, - "validation_message": null, - "width": "full" - }, - "schema": { - "name": "conversation", - "table": "project_chat_message_metadata", - "data_type": "uuid", - "default_value": null, - "max_length": null, - "numeric_precision": null, - "numeric_scale": null, - "is_nullable": true, - "is_unique": false, - "is_indexed": false, - "is_primary_key": false, - "is_generated": false, - "generation_expression": null, - "has_auto_increment": false, - "foreign_key_table": "conversation", - "foreign_key_column": "id" - } -} diff --git a/echo/directus/sync/snapshot/fields/project_chat_message_metadata/date_created.json b/echo/directus/sync/snapshot/fields/project_chat_message_metadata/date_created.json deleted file mode 100644 index 8d44b600..00000000 --- a/echo/directus/sync/snapshot/fields/project_chat_message_metadata/date_created.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "collection": "project_chat_message_metadata", - "field": "date_created", - "type": "timestamp", - "meta": { - "collection": "project_chat_message_metadata", - "conditions": null, - "display": "datetime", - "display_options": { - "relative": true - }, - "field": "date_created", - "group": null, - "hidden": true, - "interface": "datetime", - "note": null, - "options": null, - "readonly": true, - "required": false, - "searchable": true, - "sort": 2, - "special": [ - "date-created" - ], - "translations": null, - "validation": null, - "validation_message": null, - "width": "half" - }, - "schema": { - "name": "date_created", - "table": "project_chat_message_metadata", - "data_type": "timestamp with time zone", - "default_value": null, - "max_length": null, - "numeric_precision": null, - "numeric_scale": null, - "is_nullable": true, - "is_unique": false, - "is_indexed": false, - "is_primary_key": false, - "is_generated": false, - "generation_expression": null, - "has_auto_increment": false, - "foreign_key_table": null, - "foreign_key_column": null - } -} diff --git a/echo/directus/sync/snapshot/fields/project_chat_message_metadata/id.json b/echo/directus/sync/snapshot/fields/project_chat_message_metadata/id.json deleted file mode 100644 index 889f1333..00000000 --- a/echo/directus/sync/snapshot/fields/project_chat_message_metadata/id.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "collection": "project_chat_message_metadata", - "field": "id", - "type": "uuid", - "meta": { - "collection": "project_chat_message_metadata", - "conditions": null, - "display": null, - "display_options": null, - "field": "id", - "group": null, - "hidden": true, - "interface": "input", - "note": null, - "options": null, - "readonly": true, - "required": false, - "searchable": true, - "sort": 1, - "special": [ - "uuid" - ], - "translations": null, - "validation": null, - "validation_message": null, - "width": "full" - }, - "schema": { - "name": "id", - "table": "project_chat_message_metadata", - "data_type": "uuid", - "default_value": null, - "max_length": null, - "numeric_precision": null, - "numeric_scale": null, - "is_nullable": false, - "is_unique": true, - "is_indexed": false, - "is_primary_key": true, - "is_generated": false, - "generation_expression": null, - "has_auto_increment": false, - "foreign_key_table": null, - "foreign_key_column": null - } -} diff --git a/echo/directus/sync/snapshot/fields/project_chat_message_metadata/message_metadata.json b/echo/directus/sync/snapshot/fields/project_chat_message_metadata/message_metadata.json deleted file mode 100644 index 1e1fc6f4..00000000 --- a/echo/directus/sync/snapshot/fields/project_chat_message_metadata/message_metadata.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "collection": "project_chat_message_metadata", - "field": "message_metadata", - "type": "uuid", - "meta": { - "collection": "project_chat_message_metadata", - "conditions": null, - "display": null, - "display_options": null, - "field": "message_metadata", - "group": null, - "hidden": true, - "interface": "select-dropdown-m2o", - "note": null, - "options": null, - "readonly": false, - "required": false, - "searchable": true, - "sort": 4, - "special": null, - "translations": null, - "validation": null, - "validation_message": null, - "width": "full" - }, - "schema": { - "name": "message_metadata", - "table": "project_chat_message_metadata", - "data_type": "uuid", - "default_value": null, - "max_length": null, - "numeric_precision": null, - "numeric_scale": null, - "is_nullable": true, - "is_unique": false, - "is_indexed": false, - "is_primary_key": false, - "is_generated": false, - "generation_expression": null, - "has_auto_increment": false, - "foreign_key_table": "project_chat_message", - "foreign_key_column": "id" - } -} diff --git a/echo/directus/sync/snapshot/fields/project_chat_message_metadata/ratio.json b/echo/directus/sync/snapshot/fields/project_chat_message_metadata/ratio.json deleted file mode 100644 index 2c959518..00000000 --- a/echo/directus/sync/snapshot/fields/project_chat_message_metadata/ratio.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "collection": "project_chat_message_metadata", - "field": "ratio", - "type": "float", - "meta": { - "collection": "project_chat_message_metadata", - "conditions": null, - "display": null, - "display_options": null, - "field": "ratio", - "group": null, - "hidden": false, - "interface": "input", - "note": null, - "options": null, - "readonly": false, - "required": false, - "searchable": true, - "sort": 5, - "special": null, - "translations": null, - "validation": null, - "validation_message": null, - "width": "full" - }, - "schema": { - "name": "ratio", - "table": "project_chat_message_metadata", - "data_type": "real", - "default_value": null, - "max_length": null, - "numeric_precision": 24, - "numeric_scale": null, - "is_nullable": true, - "is_unique": false, - "is_indexed": false, - "is_primary_key": false, - "is_generated": false, - "generation_expression": null, - "has_auto_increment": false, - "foreign_key_table": null, - "foreign_key_column": null - } -} diff --git a/echo/directus/sync/snapshot/fields/project_chat_message_metadata/reference_text.json b/echo/directus/sync/snapshot/fields/project_chat_message_metadata/reference_text.json deleted file mode 100644 index d742b4d9..00000000 --- a/echo/directus/sync/snapshot/fields/project_chat_message_metadata/reference_text.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "collection": "project_chat_message_metadata", - "field": "reference_text", - "type": "text", - "meta": { - "collection": "project_chat_message_metadata", - "conditions": null, - "display": null, - "display_options": null, - "field": "reference_text", - "group": null, - "hidden": false, - "interface": "input-multiline", - "note": null, - "options": null, - "readonly": false, - "required": false, - "searchable": true, - "sort": 6, - "special": null, - "translations": null, - "validation": null, - "validation_message": null, - "width": "full" - }, - "schema": { - "name": "reference_text", - "table": "project_chat_message_metadata", - "data_type": "text", - "default_value": null, - "max_length": null, - "numeric_precision": null, - "numeric_scale": null, - "is_nullable": true, - "is_unique": false, - "is_indexed": false, - "is_primary_key": false, - "is_generated": false, - "generation_expression": null, - "has_auto_increment": false, - "foreign_key_table": null, - "foreign_key_column": null - } -} diff --git a/echo/directus/sync/snapshot/fields/project_chat_message_metadata/type.json b/echo/directus/sync/snapshot/fields/project_chat_message_metadata/type.json deleted file mode 100644 index 106fac34..00000000 --- a/echo/directus/sync/snapshot/fields/project_chat_message_metadata/type.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "collection": "project_chat_message_metadata", - "field": "type", - "type": "string", - "meta": { - "collection": "project_chat_message_metadata", - "conditions": null, - "display": null, - "display_options": null, - "field": "type", - "group": null, - "hidden": false, - "interface": "select-dropdown", - "note": null, - "options": { - "choices": [ - { - "text": "reference", - "value": "reference" - }, - { - "text": "citation", - "value": "citation" - } - ] - }, - "readonly": false, - "required": false, - "searchable": true, - "sort": 3, - "special": null, - "translations": null, - "validation": null, - "validation_message": null, - "width": "full" - }, - "schema": { - "name": "type", - "table": "project_chat_message_metadata", - "data_type": "character varying", - "default_value": null, - "max_length": 255, - "numeric_precision": null, - "numeric_scale": null, - "is_nullable": true, - "is_unique": false, - "is_indexed": false, - "is_primary_key": false, - "is_generated": false, - "generation_expression": null, - "has_auto_increment": false, - "foreign_key_table": null, - "foreign_key_column": null - } -} diff --git a/echo/directus/sync/snapshot/relations/project_chat_message_metadata/conversation.json b/echo/directus/sync/snapshot/relations/project_chat_message_metadata/conversation.json deleted file mode 100644 index e448cb6f..00000000 --- a/echo/directus/sync/snapshot/relations/project_chat_message_metadata/conversation.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "collection": "project_chat_message_metadata", - "field": "conversation", - "related_collection": "conversation", - "meta": { - "junction_field": null, - "many_collection": "project_chat_message_metadata", - "many_field": "conversation", - "one_allowed_collections": null, - "one_collection": "conversation", - "one_collection_field": null, - "one_deselect_action": "nullify", - "one_field": null, - "sort_field": null - }, - "schema": { - "table": "project_chat_message_metadata", - "column": "conversation", - "foreign_key_table": "conversation", - "foreign_key_column": "id", - "constraint_name": "project_chat_message_metadata_conversation_foreign", - "on_update": "NO ACTION", - "on_delete": "SET NULL" - } -} diff --git a/echo/directus/sync/snapshot/relations/project_chat_message_metadata/message_metadata.json b/echo/directus/sync/snapshot/relations/project_chat_message_metadata/message_metadata.json deleted file mode 100644 index 995c08d5..00000000 --- a/echo/directus/sync/snapshot/relations/project_chat_message_metadata/message_metadata.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "collection": "project_chat_message_metadata", - "field": "message_metadata", - "related_collection": "project_chat_message", - "meta": { - "junction_field": null, - "many_collection": "project_chat_message_metadata", - "many_field": "message_metadata", - "one_allowed_collections": null, - "one_collection": "project_chat_message", - "one_collection_field": null, - "one_deselect_action": "delete", - "one_field": "chat_message_metadata", - "sort_field": null - }, - "schema": { - "table": "project_chat_message_metadata", - "column": "message_metadata", - "foreign_key_table": "project_chat_message", - "foreign_key_column": "id", - "constraint_name": "project_chat_message_metadata_message_metadata_foreign", - "on_update": "NO ACTION", - "on_delete": "CASCADE" - } -} diff --git a/echo/server/dembrane/service/chat.py b/echo/server/dembrane/service/chat.py index d15ba544..51fcd3f7 100644 --- a/echo/server/dembrane/service/chat.py +++ b/echo/server/dembrane/service/chat.py @@ -159,7 +159,8 @@ def set_chat_name(self, chat_id: str, name: Optional[str]) -> dict: def attach_conversations(self, chat_id: str, conversation_ids: Iterable[str]) -> None: payload_list = [ - {"conversation_id": conversation_id} for conversation_id in conversation_ids + {"conversation_id": conversation_id, "project_chat_id": chat_id} + for conversation_id in conversation_ids ] if not payload_list: @@ -167,11 +168,9 @@ def attach_conversations(self, chat_id: str, conversation_ids: Iterable[str]) -> try: with self._client_context() as client: - client.update_item( - "project_chat", - chat_id, - {"used_conversations": {"create": payload_list}}, - ) + # create items directly in the junction table instead of using + # nested create through parent update, which has validation issues + client.create_item("project_chat_conversation", payload_list) except DirectusBadRequest as e: logger.error( "Failed to attach conversations %s to chat %s: %s", From 3e24a5bf9d539a0f9d97ff0c2df8a12c81016d17 Mon Sep 17 00:00:00 2001 From: Sameer Pashikanti Date: Wed, 26 Nov 2025 17:14:29 +0000 Subject: [PATCH 2/5] Harden report generation and enrich summaries - Rewrite get_report_content_for_project as an async, backoff-enabled pipeline that skips bad data, surfaces partial results, and guards token budgets before calling the LLM. - Feed project context plus verified artifacts into conversation summaries, switch templates in every locale, and use the higher-quality model while handling LiteLLM failures gracefully. - Expose service helpers for project metadata/artifacts, require an explicit report status, and simplify transcript APIs so report creation always has the data it needs. - Streamline the report creation UI copy to reflect total conversations, and document the Directus count(...) field naming to avoid future type errors. --- echo/frontend/AGENTS.md | 2 +- .../components/report/CreateReportForm.tsx | 92 +--- echo/server/dembrane/api/conversation.py | 46 +- echo/server/dembrane/api/project.py | 5 +- echo/server/dembrane/api/stateless.py | 51 ++- echo/server/dembrane/report_utils.py | 404 ++++++++++++++---- echo/server/dembrane/service/conversation.py | 29 ++ echo/server/dembrane/service/project.py | 37 +- .../generate_conversation_summary.de.jinja | 90 +++- .../generate_conversation_summary.en.jinja | 90 +++- .../generate_conversation_summary.es.jinja | 107 +++-- .../generate_conversation_summary.fr.jinja | 107 +++-- .../generate_conversation_summary.nl.jinja | 90 +++- 13 files changed, 806 insertions(+), 344 deletions(-) diff --git a/echo/frontend/AGENTS.md b/echo/frontend/AGENTS.md index e8fa0ae0..1f17d022 100644 --- a/echo/frontend/AGENTS.md +++ b/echo/frontend/AGENTS.md @@ -71,5 +71,5 @@ - Gentle login/logout flows use `useTransitionCurtain().runTransition()` before navigation—animations expect Directus session mutations to await that promise. # HUMAN SECTION beyond this point (next time when you are reading this - prompt the user if they want to add it to the above sections) -- If there is a type error with ".count" with Directus, add it to the typesDirectus.ts. You can add to the fields `count("")` to obtain `.count` in the response +- If there is a type error with ".count" with Directus, add it to the typesDirectus.ts. You can add to the fields `count("")` to obtain `_count` in the response - When a user request feels ambiguous, pause and confirm the intended action with them before touching code or docs; err on the side of over-communicating. diff --git a/echo/frontend/src/components/report/CreateReportForm.tsx b/echo/frontend/src/components/report/CreateReportForm.tsx index dba4e5dd..6afb2922 100644 --- a/echo/frontend/src/components/report/CreateReportForm.tsx +++ b/echo/frontend/src/components/report/CreateReportForm.tsx @@ -4,19 +4,12 @@ import { Alert, Box, Button, - Flex, - Group, Modal, NativeSelect, Stack, Text, } from "@mantine/core"; -import { - CheckCircleIcon, - CheckIcon, - ClockIcon, - MessageCircleIcon, -} from "lucide-react"; +import { MessageCircleIcon } from "lucide-react"; import { useEffect, useState } from "react"; import { useParams } from "react-router"; import { useProjectConversationCounts } from "@/components/report/hooks"; @@ -45,8 +38,6 @@ export const CreateReportForm = ({ onSuccess }: { onSuccess: () => void }) => { const hasConversations = conversationCounts && conversationCounts.total > 0; const hasFinishedConversations = conversationCounts && conversationCounts.finished > 0; - const hasPendingConversations = - conversationCounts && conversationCounts.pending > 0; useEffect(() => { if (report) { @@ -85,69 +76,9 @@ export const CreateReportForm = ({ onSuccess }: { onSuccess: () => void }) => { {/* Conversation Status Section */} {hasConversations ? ( - <> - - Generate insights from your conversations - - - {conversationCounts.pending !== 0 && ( - - - {/* Title Row */} - - - Your Conversations - - - {conversationCounts.total} total - - - - {/* Ready Row - only show if there are finished conversations */} - {hasFinishedConversations && ( - - - - - {conversationCounts.pending === 0 ? ( - All conversations ready - ) : ( - - {conversationCounts.finished}{" "} - {conversationCounts.finished === 1 - ? t`conversation` - : t`conversations`}{" "} - ready - - )} - - - - - )} - - {/* Processing Row - only show if there are pending conversations */} - {hasPendingConversations && ( - - - - - {conversationCounts.pending}{" "} - {conversationCounts.pending === 1 - ? t`conversation` - : t`conversations`}{" "} - processing - - - - ~30 min - - - )} - - - )} - + + Generate insights from your conversations + ) : ( /* No conversations message */ @@ -166,7 +97,7 @@ export const CreateReportForm = ({ onSuccess }: { onSuccess: () => void }) => { )} {/* Detailed Conversation Modal */} - {hasConversations && hasPendingConversations && ( + {hasConversations && ( <> @@ -182,19 +113,12 @@ export const CreateReportForm = ({ onSuccess }: { onSuccess: () => void }) => { }} className="cursor-pointer underline-offset-4 hover:underline" > - {conversationCounts.finished} conversations{" "} + {conversationCounts.total ?? 0} + conversations + {" "} will be included in your report - - {hasPendingConversations && ( - - - In the meantime, if you want to analyze the conversations that - are still processing, you can use the Chat feature - - - )} str: +async def get_conversation_transcript(conversation_id: str, auth: DependencyDirectusSession) -> str: # svc = conversation_service_for_auth(auth) active_client = auth.client or directus await raise_if_conversation_not_found_or_not_authorized(conversation_id, auth) - if include_project_data: - # TODO: implement this - logger.warning("Not implemented yet") - conversation_chunks = await run_in_thread_pool( active_client.get_items, "conversation_chunk", @@ -393,7 +387,6 @@ async def get_conversation_transcript( transcript = [] for chunk in conversation_chunks: - logger.debug(f"Chunk transcript: {chunk['transcript']}") if chunk["transcript"]: transcript.append(chunk["transcript"]) @@ -465,6 +458,7 @@ async def generate() -> AsyncGenerator[str, None]: ) +# this should ideally be in the service. the async functions are a bit messy at the moment. @ConversationRouter.post("/{conversation_id}/summarize", response_model=None) async def summarize_conversation( conversation_id: str, @@ -474,30 +468,40 @@ async def summarize_conversation( await raise_if_conversation_not_found_or_not_authorized(conversation_id, auth) - conversation_data = await run_in_thread_pool( + conversation_data_result = await run_in_thread_pool( active_client.get_items, "conversation", { "query": { - "filter": { - "id": {"_eq": conversation_id}, - }, + "filter": {"id": {"_eq": conversation_id}}, "fields": ["id", "project_id.language"], }, }, ) - if not conversation_data or len(conversation_data) == 0: + awaitable_list = [ + get_conversation_transcript(conversation_id, auth), + run_in_thread_pool( + project_service.get_context_for_prompt, + conversation_data_result[0]["project_id"], + ), + run_in_thread_pool( + conversation_service.get_verified_artifacts, + conversation_id, + ), + ] + + transcript_str, project_context_str, verified_artifacts = await asyncio.gather(*awaitable_list) + + if not conversation_data_result or len(conversation_data_result) == 0: raise HTTPException(status_code=404, detail="Conversation not found") + conversation_data = conversation_data_result + conversation_data = conversation_data[0] language = conversation_data["project_id"]["language"] - transcript_str = await get_conversation_transcript( - conversation_id, auth, include_project_data=True - ) - if transcript_str == "": return { "status": "success", @@ -505,7 +509,11 @@ async def summarize_conversation( } else: summary = await run_in_thread_pool( - generate_summary, transcript_str, language if language else "en" + generate_summary, + transcript_str, + language if language else "en", + project_context_str, + verified_artifacts, ) await run_in_thread_pool( diff --git a/echo/server/dembrane/api/project.py b/echo/server/dembrane/api/project.py index ebe86299..9863686e 100644 --- a/echo/server/dembrane/api/project.py +++ b/echo/server/dembrane/api/project.py @@ -362,10 +362,7 @@ async def create_report( raise e report = await run_in_thread_pool( - project_service.create_report, - project_id, - language, - report_content_response, + project_service.create_report, project_id, language, report_content_response, "archived" ) return report diff --git a/echo/server/dembrane/api/stateless.py b/echo/server/dembrane/api/stateless.py index 0604ed8f..326688a0 100644 --- a/echo/server/dembrane/api/stateless.py +++ b/echo/server/dembrane/api/stateless.py @@ -15,13 +15,20 @@ StatelessRouter = APIRouter(tags=["stateless"]) -def generate_summary(transcript: str, language: str | None) -> str: +def generate_summary( + transcript: str, + language: str | None, + project_context: str | None = None, + verified_artifacts: list[str] | None = None, +) -> str: """ Generate a summary of the transcript using LangChain and a custom API endpoint. Args: transcript (str): The conversation transcript to summarize. language (str | None): The language of the transcript. + project_context (str | None): Optional project context to include. + verified_artifacts (list[str] | None): Optional list of verified artifacts. Returns: str: The generated summary. @@ -30,23 +37,37 @@ def generate_summary(transcript: str, language: str | None) -> str: prompt = render_prompt( "generate_conversation_summary", language if language else "en", - {"quote_text_joined": transcript}, + { + "quote_text_joined": transcript, + "project_context": project_context, + # Pass empty list instead of None for Jinja iteration safety + "verified_artifacts": verified_artifacts or [], + }, ) - # Call the model over the provided API endpoint - response = completion( - messages=[ - { - "content": prompt, - "role": "user", - } - ], - **get_completion_kwargs(MODELS.TEXT_FAST), - ) - - response_content = response["choices"][0]["message"]["content"] + try: + response = completion( + messages=[ + { + "role": "user", + "content": prompt, + } + ], + **get_completion_kwargs(MODELS.MULTI_MODAL_PRO), + ) + except Exception as e: + logger.error(f"LiteLLM completion error: {e}") + raise - return response_content + try: + response_content = response.choices[0].message.content + if response_content is None: + logger.warning("LLM returned None content for summary") + return "" + return response_content + except (IndexError, AttributeError, KeyError) as e: + logger.error(f"Error getting response content for summary: {e}") + return "" def validate_segment_id(echo_segment_ids: list[str] | None) -> bool: diff --git a/echo/server/dembrane/report_utils.py b/echo/server/dembrane/report_utils.py index 18916cb9..665175c7 100644 --- a/echo/server/dembrane/report_utils.py +++ b/echo/server/dembrane/report_utils.py @@ -1,13 +1,26 @@ import re +import asyncio import logging +from typing import Optional -from litellm import completion +import backoff +import sentry_sdk +from litellm import acompletion from litellm.utils import token_counter, get_model_info +from litellm.exceptions import ( + Timeout, + APIError, + RateLimitError, + BadRequestError, + ContextWindowExceededError, + ContentPolicyViolationError, +) from dembrane.llms import MODELS, get_completion_kwargs from dembrane.prompts import render_prompt -from dembrane.directus import directus -from dembrane.api.conversation import get_conversation_transcript +from dembrane.directus import DirectusGenericException, directus +from dembrane.async_helpers import run_in_thread_pool +from dembrane.api.conversation import summarize_conversation, get_conversation_transcript from dembrane.api.dependency_auth import DirectusSession logger = logging.getLogger("report_utils") @@ -28,6 +41,9 @@ f"Using {TEXT_PROVIDER_MODEL} for report generation with context length {MAX_REPORT_CONTEXT_LENGTH}" ) +# Default timeout for LLM report generation (5 minutes) +REPORT_GENERATION_TIMEOUT = 5 * 60 + class ContextTooLongException(Exception): """Exception raised when the context length exceeds the maximum allowed.""" @@ -35,47 +51,231 @@ class ContextTooLongException(Exception): pass -async def get_report_content_for_project(project_id: str, language: str) -> str: - conversations = directus.get_items( - "conversation", - { - "query": { - "filter": { - "project_id": project_id, +class ReportGenerationError(Exception): + """Exception raised when report generation fails after retries.""" + + def __init__(self, message: str, cause: Optional[Exception] = None): + super().__init__(message) + self.cause = cause + + +@backoff.on_exception( + backoff.expo, + (RateLimitError, Timeout, APIError), + max_tries=3, + max_time=REPORT_GENERATION_TIMEOUT, + on_backoff=lambda details: logger.warning( + f"Retrying LLM call for report generation (attempt {details['tries']}): {details.get('exception')}" + ), +) +async def _call_llm_for_report(prompt: str) -> str: + """Call LLM with automatic retry for transient errors.""" + logger.debug("Calling LLM for report generation") + response = await acompletion( + messages=[{"role": "user", "content": prompt}], + timeout=REPORT_GENERATION_TIMEOUT, + **get_completion_kwargs(MODELS.TEXT_FAST), + ) + return response.choices[0].message.content + + +async def _safe_summarize_conversation(conversation_id: str) -> dict: + """ + Safely summarize a single conversation, catching and logging errors. + Returns a dict with 'success' and optionally 'error' fields. + """ + try: + await summarize_conversation( + conversation_id, auth=DirectusSession(user_id="none", is_admin=True) + ) + return {"success": True, "conversation_id": conversation_id} + except Exception as e: + logger.warning( + f"Failed to summarize conversation {conversation_id}: {type(e).__name__}: {e}" + ) + return {"success": False, "conversation_id": conversation_id, "error": str(e)} + + +async def _safe_get_transcript(conversation_id: str) -> Optional[str]: + """ + Safely get transcript for a conversation, returning None on error. + """ + try: + transcript = await get_conversation_transcript( + conversation_id, + DirectusSession(user_id="none", is_admin=True), + ) + return transcript or "" + except Exception as e: + logger.warning( + f"Failed to get transcript for conversation {conversation_id}: {type(e).__name__}: {e}" + ) + return None + + +async def _fetch_conversations(project_id: str, fields: list) -> list: + """ + Fetch conversations for a project with error handling. + """ + try: + conversations = await run_in_thread_pool( + directus.get_items, + "conversation", + { + "query": { + "filter": {"project_id": {"_eq": project_id}}, + "fields": fields, + "sort": "-updated_at", }, - "fields": [ - "id", - "participant_name", - "tags.project_tag_id.text", - "summary", - "created_at", - "updated_at", - ], - # Sort by updated_at descending to get most recent conversations first - "sort": "-updated_at", - } - }, + }, + ) + return conversations if conversations else [] + except DirectusGenericException as e: + logger.error(f"Directus error fetching conversations for project {project_id}: {e}") + sentry_sdk.capture_exception(e) + raise ReportGenerationError( + f"Failed to fetch conversations from database: {e}", cause=e + ) from e + except Exception as e: + logger.error(f"Unexpected error fetching conversations for project {project_id}: {e}") + sentry_sdk.capture_exception(e) + raise ReportGenerationError(f"Unexpected error fetching conversations: {e}", cause=e) from e + + +async def get_report_content_for_project(project_id: str, language: str) -> str: + """ + Generate a report for a project based on conversation summaries and transcripts. + + This function is fault-tolerant and will: + - Continue processing if individual conversation summaries fail + - Skip conversations that cannot be fetched + - Retry LLM calls on transient failures + - Return partial results if some data is unavailable + + Args: + project_id: The ID of the project to generate a report for + language: The language code for the report + + Returns: + The generated report content as a string + + Raises: + ReportGenerationError: If the report cannot be generated due to critical failures + """ + # Initial fetch to get conversations with chunk counts + conversations = await _fetch_conversations( + project_id, + fields=["id", "summary", "count(chunks)"], ) - logger.debug(f"Found {len(conversations)} conversations for project {project_id}") - logger.debug(f"Conversations: {conversations}") + logger.debug(f"Initially found {len(conversations)} conversations for project {project_id}") - token_count = 0 + if len(conversations) == 0: + logger.info(f"No conversations found for project {project_id}") + return "No conversations found for project" + + # Filter to conversations with chunks that need summarization + conversation_with_chunks = [] + for conversation in conversations: + try: + chunks_count = int(conversation.get("chunks_count", 0)) + if chunks_count > 0: + conversation_with_chunks.append(conversation) + logger.debug(f"Conversation {conversation['id']} has {chunks_count} chunks") + except (ValueError, TypeError) as e: + logger.warning(f"Invalid chunks_count for conversation {conversation.get('id')}: {e}") + continue + + if not conversation_with_chunks: + logger.info(f"No conversations with chunks found for project {project_id}") + return "No conversations with content found for project" + + logger.info(f"Generating summaries for {len(conversation_with_chunks)} conversations") + + # Batch summarization with fault tolerance + batch_size = 5 + total_summary_failures = 0 + + for i in range(0, len(conversation_with_chunks), batch_size): + batch = conversation_with_chunks[i : i + batch_size] + tasks = [_safe_summarize_conversation(conv["id"]) for conv in batch] + + # Use return_exceptions=True to prevent one failure from canceling others + results = await asyncio.gather(*tasks, return_exceptions=True) + + for result in results: + if isinstance(result, Exception): + logger.error(f"Unexpected exception during batch summarization: {result}") + total_summary_failures += 1 + elif isinstance(result, dict) and not result.get("success"): + total_summary_failures += 1 + + if total_summary_failures > 0: + logger.warning( + f"Failed to summarize {total_summary_failures}/{len(conversation_with_chunks)} conversations" + ) + + # Refetch conversations with all needed fields + try: + conversations = await _fetch_conversations( + project_id, + fields=[ + "id", + "participant_name", + "tags.project_tag_id.text", + "summary", + "created_at", + "updated_at", + "count(chunks)", + ], + ) + except ReportGenerationError: + # If refetch fails, we can't proceed + raise + + if not conversations: + logger.warning(f"No conversations found on refetch for project {project_id}") + return "No conversations available for report" + + # Build conversation data with token budget management conversation_data_dict: dict = {} + token_count = 0 + skipped_no_chunks = 0 + skipped_no_summary = 0 + skipped_token_limit = 0 - # first add all the summaries to the list for conversation in conversations: - logger.info(f"Adding conversation {conversation['id']} to report") + conv_id = conversation.get("id") + if not conv_id: + logger.warning("Conversation missing 'id' field, skipping") + continue - if conversation["summary"] is None: - logger.info(f"Conversation {conversation['id']} has no summary") + try: + chunks_count = int(conversation.get("chunks_count", 0)) + except (ValueError, TypeError): + chunks_count = 0 + + if chunks_count == 0: + logger.debug(f"Conversation {conv_id} has no chunks, skipping") + skipped_no_chunks += 1 + continue + + summary = conversation.get("summary") + if not summary: + logger.debug(f"Conversation {conv_id} has no summary, skipping") + skipped_no_summary += 1 continue # Count tokens before adding - summary_tokens = token_counter( - messages=[{"role": "user", "content": conversation["summary"]}], - model=get_completion_kwargs(MODELS.TEXT_FAST)["model"], - ) + try: + summary_tokens = token_counter( + messages=[{"role": "user", "content": summary}], + model=TEXT_PROVIDER_MODEL, + ) + except Exception as e: + logger.warning(f"Failed to count tokens for conversation {conv_id}: {e}") + # Use a rough estimate: ~4 chars per token + summary_tokens = len(summary) // 4 # Check if adding this conversation would exceed the limit if token_count + summary_tokens >= MAX_REPORT_CONTEXT_LENGTH: @@ -83,28 +283,28 @@ async def get_report_content_for_project(project_id: str, language: str) -> str: f"Reached context limit. Added {len(conversation_data_dict)} conversations. " f"Token count: {token_count}, attempted to add: {summary_tokens}" ) + skipped_token_limit += 1 break # Guard against missing or null tags coming back from the API - try: - tags = conversation["tags"] or [] - except KeyError: - tags = [] - tags_text = "" - for tag in tags: - # In some older DB dumps we have seen empty tag objects – guard against that too - try: - tag_text = tag["project_tag_id"]["text"] - tags_text += tag_text + ", " - except (KeyError, TypeError): - continue + try: + tags = conversation.get("tags") or [] + for tag in tags: + if tag and isinstance(tag, dict): + project_tag = tag.get("project_tag_id") + if project_tag and isinstance(project_tag, dict): + tag_text = project_tag.get("text") + if tag_text: + tags_text += tag_text + ", " + except (KeyError, TypeError, AttributeError) as e: + logger.debug(f"Error processing tags for conversation {conv_id}: {e}") # Add the conversation after confirming it fits - conversation_data_dict[conversation["id"]] = { - "name": conversation["participant_name"], - "tags": tags_text, - "transcript": conversation["summary"], + conversation_data_dict[conv_id] = { + "name": conversation.get("participant_name"), + "tags": tags_text.rstrip(", "), + "transcript": summary, "created_at": conversation.get("created_at"), "updated_at": conversation.get("updated_at"), } @@ -112,39 +312,45 @@ async def get_report_content_for_project(project_id: str, language: str) -> str: # Now try to add full transcripts for conversations that have summaries # Process in the same order (most recent first) + transcripts_added = 0 + transcripts_skipped = 0 + for conversation in conversations: - # Only attempt to append a transcript if the conversation was added during the - # first pass (i.e. it had a non-empty summary). - if conversation["id"] not in conversation_data_dict: + conv_id = conversation.get("id") + if not conv_id or conv_id not in conversation_data_dict: continue - transcript = await get_conversation_transcript( - conversation["id"], - DirectusSession(user_id="none", is_admin=True), - ) + transcript = await _safe_get_transcript(conv_id) + if transcript is None: + # Error already logged in _safe_get_transcript + transcripts_skipped += 1 + continue - # Gracefully handle a null / empty transcript - transcript = transcript or "" if transcript == "": - logger.info(f"Conversation {conversation['id']} has empty transcript – skipping") + logger.debug(f"Conversation {conv_id} has empty transcript – skipping") continue # Calculate token count for the transcript - transcript_tokens = token_counter( - messages=[{"role": "user", "content": transcript}], - model=get_completion_kwargs(MODELS.TEXT_FAST)["model"], - ) + try: + transcript_tokens = token_counter( + messages=[{"role": "user", "content": transcript}], + model=TEXT_PROVIDER_MODEL, + ) + except Exception as e: + logger.warning(f"Failed to count transcript tokens for {conv_id}: {e}") + transcript_tokens = len(transcript) // 4 if token_count + transcript_tokens < MAX_REPORT_CONTEXT_LENGTH: # Append with a newline to keep paragraphs separated - conversation_data_dict[conversation["id"]]["transcript"] += "\n" + transcript + conversation_data_dict[conv_id]["transcript"] += "\n" + transcript token_count += transcript_tokens - logger.info( - f"Added transcript for conversation {conversation['id']}. Total tokens: {token_count}" + transcripts_added += 1 + logger.debug( + f"Added transcript for conversation {conv_id}. Total tokens: {token_count}" ) else: - logger.info( - f"Cannot add transcript for conversation {conversation['id']}. " + logger.debug( + f"Cannot add transcript for conversation {conv_id}. " f"Would exceed limit: {token_count} + {transcript_tokens} > {MAX_REPORT_CONTEXT_LENGTH}" ) # Since conversations are sorted by recency, if we can't fit this one, @@ -153,8 +359,15 @@ async def get_report_content_for_project(project_id: str, language: str) -> str: conversation_data_list = list(conversation_data_dict.values()) + if not conversation_data_list: + logger.warning(f"No usable conversations for report in project {project_id}") + return "No conversations with sufficient content available for report generation" + logger.info( - f"Report for project {project_id} will include {len(conversation_data_list)} conversations. " + f"Report for project {project_id} will include {len(conversation_data_list)} conversations " + f"(skipped: {skipped_no_chunks} no chunks, {skipped_no_summary} no summary, " + f"{skipped_token_limit} token limit). " + f"Transcripts: {transcripts_added} added, {transcripts_skipped} failed. " f"Total token count: {token_count} of {MAX_REPORT_CONTEXT_LENGTH} allowed." ) @@ -162,28 +375,53 @@ async def get_report_content_for_project(project_id: str, language: str) -> str: "system_report", language, {"conversations": conversation_data_list} ) - # Use the configured Litellm provider for report generation - response = completion( - messages=[ - {"role": "user", "content": prompt_message}, - # Some providers expect a prefilled assistant message; add if needed. - # {"role": "assistant", "content": "
"}, - ], - **get_completion_kwargs(MODELS.TEXT_FAST), - ) - - response_content = response.choices[0].message.content + # Use the configured Litellm provider for report generation with retry + try: + response_content = await _call_llm_for_report(prompt_message) + except ContextWindowExceededError as e: + logger.error(f"Context window exceeded for project {project_id}: {e}") + sentry_sdk.capture_exception(e) + raise ReportGenerationError( + "Report content too large for the language model", cause=e + ) from e + except ContentPolicyViolationError as e: + logger.error(f"Content policy violation for project {project_id}: {e}") + sentry_sdk.capture_exception(e) + raise ReportGenerationError("Report content violates content policy", cause=e) from e + except BadRequestError as e: + logger.error(f"Bad request error for project {project_id}: {e}") + sentry_sdk.capture_exception(e) + raise ReportGenerationError(f"Invalid request to language model: {e}", cause=e) from e + except (RateLimitError, Timeout, APIError) as e: + # These are retried by backoff, so if we get here, all retries failed + logger.error(f"LLM call failed after retries for project {project_id}: {e}") + sentry_sdk.capture_exception(e) + raise ReportGenerationError( + f"Report generation failed after multiple retries: {type(e).__name__}", cause=e + ) from e + except Exception as e: + logger.error(f"Unexpected error during LLM call for project {project_id}: {e}") + sentry_sdk.capture_exception(e) + raise ReportGenerationError( + f"Unexpected error during report generation: {e}", cause=e + ) from e + + if not response_content: + logger.warning(f"Empty response from LLM for project {project_id}") + return "Report generation returned empty content" # Extract content between
tags article_pattern = r"
(.*?)
" match = re.search(article_pattern, response_content, re.DOTALL) if match: - response_content = match.group(1) + response_content = match.group(1).strip() else: # If no
tags found, keep original content but remove any existing tags - response_content = response_content.replace("
", "").replace("
", "") + response_content = ( + response_content.replace("
", "").replace("
", "").strip() + ) - logger.debug(f"Report content for project {project_id}: {response_content}") + logger.debug(f"Report content for project {project_id}: {response_content[:200]}...") return response_content diff --git a/echo/server/dembrane/service/conversation.py b/echo/server/dembrane/service/conversation.py index 8472565f..4c529072 100644 --- a/echo/server/dembrane/service/conversation.py +++ b/echo/server/dembrane/service/conversation.py @@ -588,3 +588,32 @@ def _list_conversations( raise ConversationServiceException() from e return conversations or [] + + def get_verified_artifacts(self, conversation_id: str, limit: int = 3) -> List[dict]: + try: + with self._client_context() as client: + artifacts = client.get_items( + "conversation_artifact", + { + "query": { + "filter": { + "conversation_id": conversation_id, + "approved_at": {"_nnull": True}, + }, + "fields": ["id", "key", "content"], + "sort": "-approved_at", + "limit": limit, + } + }, + ) + except DirectusBadRequest as e: + logger.error( + "Failed to get verified artifacts for conversation %s via Directus: %s", + conversation_id, + e, + ) + raise ConversationServiceException() from e + except (KeyError, IndexError) as e: + raise ConversationServiceException() from e + + return artifacts or [] diff --git a/echo/server/dembrane/service/project.py b/echo/server/dembrane/service/project.py index ef1d800b..6383908e 100644 --- a/echo/server/dembrane/service/project.py +++ b/echo/server/dembrane/service/project.py @@ -103,7 +103,7 @@ def create_report( project_id: str, language: str, content: str, - status: str = "archived", + status: str, error_code: Optional[str] = None, ) -> dict: payload = { @@ -117,6 +117,7 @@ def create_report( payload["error_code"] = error_code with directus_client_context(self.directus_client) as client: + self.logger.info(f"Creating report for project {project_id} with status: {status}") report = client.create_item("project_report", item_data=payload)["data"] return report @@ -229,3 +230,37 @@ def create_shallow_clone( self.create_tags_and_link(new_project["id"], tag_str_list) return new_project["id"] + + def get_context_for_prompt(self, project_id: str) -> str | None: + project = self.get_by_id_or_raise(project_id) + + builder = [] + + if project["name"]: + builder.append("name: " + project["name"]) + + if project["context"]: + builder.append("context: " + project["context"]) + + if project["default_conversation_transcript_prompt"]: + builder.append( + "hotwords that the user set: " + project["default_conversation_transcript_prompt"] + ) + + ### Portal details + if project["default_conversation_title"]: + builder.append( + "default title that was shown to the user (not always relevant but might add context): " + + project["default_conversation_title"] + ) + + if project["default_conversation_description"]: + builder.append( + "default question that was shown to the user (not always relevant but might add context): " + + project["default_conversation_description"] + ) + + if len(builder) > 0: + return "project context: " + "\n".join(builder) + else: + return None diff --git a/echo/server/prompt_templates/generate_conversation_summary.de.jinja b/echo/server/prompt_templates/generate_conversation_summary.de.jinja index b1ca442b..a7a9373a 100644 --- a/echo/server/prompt_templates/generate_conversation_summary.de.jinja +++ b/echo/server/prompt_templates/generate_conversation_summary.de.jinja @@ -4,33 +4,83 @@ Ihnen wird eine Reihe von Zitaten aus einem Gesprächstranskript in chronologisc {{ quote_text_joined }} -Erstellen Sie eine Zusammenfassung, die sich an die Komplexität des Inhalts anpasst und dabei sowohl Substanz als auch Nuancen erfasst. +{% if project_context %} + +Der Gastgeber hat den folgenden Kontext und Schlüsselbegriffe für dieses Gespräch bereitgestellt: +{{ project_context }} + +{% endif %} -## Kernrichtlinien +{% if verified_artifacts %} + +Während dieses Gesprächs haben die Teilnehmer gemeinsam die folgenden konkreten Ergebnisse erstellt und verifiziert: +{% for artifact in verified_artifacts %} -1. **Proportionale Reaktion**: Die Länge der Zusammenfassung darf die Länge des ursprünglichen Inhalts nie überschreiten. Für sehr einfache Eingaben geben Sie eine noch kürzere Zusammenfassung. +--- +{{ artifact }} +--- +{% endfor %} + +{% endif %} -2. **Inhaltsgetriebene Struktur**: - - Für einfache Austausche (Erklärungen, Witze, Klärungen): Verwenden Sie 1-2 prägnante Absätze ohne formale Abschnitte - - Für komplexe Diskussionen: Verwenden Sie geeignete Überschriften und Struktur +Erstellen Sie eine Zusammenfassung, die die Essenz und Bedeutung des Geschehenen erfasst, optimiert für schnelle Erfassbarkeit. -3. **Essenz über Analyse**: Erfassen Sie das tatsächlich Gesagte, ohne spekulative Kontexte, Hintergründe oder Implikationen hinzuzufügen, es sei denn, sie werden ausdrücklich erwähnt. +## Kernprinzipien -4. **Menschliche Elemente**: Bewahren Sie Humor, Wortspiele oder emotionalen Kontext, wenn sie für die Bedeutung des Gesprächs zentral sind. +1. **Essenz über Vollständigkeit**: Erfassen Sie das "Na und?" - das bedeutungsvolle Ergebnis, die Entscheidung oder Erkenntnis - nicht einen detaillierten Bericht -5. **Anpassbare Tiefe**: - - Einfache Inhalte erhalten einfache Zusammenfassungen - - Komplexe Inhalte erhalten angemessen strukturierte Analysen - - Überfrachten Sie einfache Inhalte nie mit unnötigen akademischen oder strategischen Rahmen +2. **Radikale Kürze**: Die meisten Gespräche sollten in 2-4 Sätzen zusammengefasst werden. Komplexe Diskussionen können 1-2 kurze Absätze rechtfertigen. Die Zusammenfassung sollte immer dramatisch kürzer sein als das Original. -## Formatierungsanforderungen +3. **Bedeutungsvolle Kompression**: Fragen Sie "Was müsste jemand wissen, der dies verpasst hat?" nicht "Was wurde besprochen?" -- Für sehr einfache Eingaben (unter 100 Wörtern) beschränken Sie die Zusammenfassung auf 1-3 Sätze -- Für mittlere Eingaben verwenden Sie 1-2 prägnante Absätze -- Verwenden Sie Abschnittsüberschriften nur für wirklich komplexe, mehrthematische Gespräche -- Heben Sie nur wirklich bedeutende Punkte hervor, keine Routinebeobachtungen -- Präsentieren Sie als eine abgerundete, zugängliche Zusammenfassung, die für sich steht +4. **Natürliche Sprache**: Schreiben Sie in fließender Prosa, nicht in Aufzählungspunkten oder strukturierten Abschnitten. Vermeiden Sie Überschriften, Listen und Formatierung, es sei denn, der Inhalt ist wirklich komplex und vielschichtig. -Denken Sie daran, dass effektive Zusammenfassung oft bedeutet: signifikante Reduzierung der Länge bei Beibehaltung der Kernbedeutung. +5. **Kontextintegration**: + - Weben Sie Projektkontext natürlich ein, wenn er Bedeutung hinzufügt + - Erwähnen Sie verifizierte Artefakte als konkrete Ergebnisse ohne Prozessdetails + - Erzwingen Sie keinen irrelevanten Kontext -Antworten Sie auf Deutsch, unabhängig von der Sprache vorheriger Nachrichten. Sie können Markdown-Formatierung maßvoll zur Verbesserung der Lesbarkeit verwenden, priorisieren Sie aber erzählerische Kohärenz und Absätze über strukturelle Elemente. \ No newline at end of file +6. **Schnelle Lese-Bewertung**: Bewerten Sie vor dem Schreiben intern, wie schnell diese Zusammenfassung zu erfassen ist, auf einer Skala von 1-10: + - 10 = Sofortiges Verständnis (einfach, ein klarer Punkt) + - 7-9 = Sehr schneller Überblick (2-3 klare Erkenntnisse) + - 4-6 = Erfordert fokussiertes Lesen (mehrere Punkte oder mittlere Komplexität) + - 1-3 = Erfordert sorgfältiges Lesen (dicht, komplex oder nuanciert) + + Streben Sie 7+ an, wann immer möglich. Wenn Ihr Entwurf unter 7 liegt, vereinfachen Sie weiter. + +## Was Erfasst Werden Soll + +Konzentrieren Sie sich auf das Wichtigste. Schließen Sie diese Elemente ein, wenn sie zentral für das Gespräch sind: + +- Getroffene Entscheidungen und ihre Begründung +- Identifizierte Probleme und vereinbarte Lösungen +- Wichtige Erkenntnisse oder Realisierungen +- Konkrete nächste Schritte oder Ergebnisse +- Kerndynamiken, wenn sie das Ergebnis geprägt haben: Argumente oder Überlegungen, die Entscheidungen beeinflussten, Spannungen zwischen konkurrierenden Prioritäten, Konflikte, die gelöst werden mussten, oder erzielte Einigungen bei strittigen Punkten +- Bedeutungsvolle Meinungsverschiedenheiten oder besprochene Kompromisse + +## Was Übersprungen Werden Soll + +- Detaillierter Bericht der Diskussion +- Implementierungsdetails, es sei denn, sie sind der Hauptpunkt +- Prozessmechanik (wer was sagte, wie Entscheidungen getroffen wurden) +- Offensichtlicher Kontext oder Hintergrund +- Kleinere Meinungsverschiedenheiten, die keine Auswirkungen auf die Ergebnisse hatten + +## Stilrichtlinien + +- Verwenden Sie Kommas, Klammern oder "aber" anstelle von Gedankenstrichen +- Schreiben Sie in direkter, gesprächiger Prosa +- Bevorzugen Sie kürzere Sätze gegenüber komplexer Zeichensetzung +- Verwenden Sie "aber" oder "jedoch", um Kontraste zu zeigen +- Verwenden Sie Klammern für Einschübe oder Klarstellungen +- Verwenden Sie niemals Fettdruck, Kursivschrift oder andere Textformatierung + +## Format + +- 2-4 Sätze für einfache Gespräche +- 1-2 prägnante Absätze für mittlere Komplexität +- Natürliche Prosa ohne Aufzählungspunkte, Überschriften, Listen oder Formatierung +- Verwenden Sie nur Struktur, wenn sie für eine vielschichtige Diskussion wirklich notwendig ist + +Schreiben Sie, als würden Sie einem Kollegen in 30 Sekunden erklären, was passiert ist. diff --git a/echo/server/prompt_templates/generate_conversation_summary.en.jinja b/echo/server/prompt_templates/generate_conversation_summary.en.jinja index 3c73d532..74452159 100644 --- a/echo/server/prompt_templates/generate_conversation_summary.en.jinja +++ b/echo/server/prompt_templates/generate_conversation_summary.en.jinja @@ -4,31 +4,83 @@ You are given a series of quotes from a conversation transcript in chronological {{ quote_text_joined }} -Create a summary that adapts to the content's complexity while capturing both substance and nuance. +{% if project_context %} + +The host has provided the following context and key terms for this conversation: +{{ project_context }} + +{% endif %} -## Core Guidelines +{% if verified_artifacts %} + +During this conversation, participants collaboratively created and verified the following concrete outcomes: +{% for artifact in verified_artifacts %} -1. **Proportional Response**: The summary length should never exceed the original content length. For very simple inputs, provide an even shorter summary. +--- +{{ artifact }} +--- +{% endfor %} + +{% endif %} -2. **Content-Driven Structure**: - - For simple exchanges (explanations, jokes, clarifications): Use 1-2 concise paragraphs without formal sections - - For complex discussions: Use appropriate headers and structure +Create a summary that captures the essence and meaning of what happened, optimized for glanceability. -3. **Essence Over Analysis**: Capture what was actually said without adding speculative context, background, or implications unless explicitly mentioned. +## Core Principles -4. **Human Elements**: Preserve humor, wordplay, or emotional context when central to the conversation's meaning. +1. **Essence over exhaustiveness**: Capture the "so what?" - the meaningful outcome, decision, or insight - not a blow-by-blow account -5. **Adaptable Depth**: - - Simple content receives simple summaries - - Complex content receives appropriately structured analysis - - Never inflate basic content with unnecessary academic or strategic framing +2. **Radical conciseness**: Most conversations should summarize to 2-4 sentences. Complex discussions might warrant 1-2 short paragraphs. The summary should always be dramatically shorter than the original. -## Format Requirements +3. **Meaningful compression**: Ask "what would someone need to know if they missed this?" not "what was discussed?" -- For very simple inputs (under 100 words), limit summary to 1-3 sentences -- For moderate inputs, use 1-2 concise paragraphs -- Only use section headers for genuinely complex, multi-topic conversations -- Bold genuinely significant points, not routine observations -- Present as a finished, accessible summary that stands on its own +4. **Natural language**: Write in flowing prose, not bullet points or structured sections. Avoid headers, lists, and formatting unless the content is genuinely complex and multi-faceted. -Remember that effective summarization often means significant reduction in length while preserving core meaning. \ No newline at end of file +5. **Context integration**: + - Weave in project context naturally when it adds meaning + - Note verified artifacts as concrete outcomes without process detail + - Don't force irrelevant context + +6. **Fast reading score**: Before writing, internally assess how glanceable this summary will be on a scale of 1-10: + - 10 = Instant comprehension (simple, single clear point) + - 7-9 = Very quick scan (2-3 clear takeaways) + - 4-6 = Needs focused reading (multiple points or moderate complexity) + - 1-3 = Requires careful reading (dense, complex, or nuanced) + + Aim for 7+ whenever possible. If your draft scores below 7, simplify further. + +## What to Capture + +Focus on what matters most. Include these elements when they're central to the conversation: + +- Decisions made and their rationale +- Problems identified and solutions agreed upon +- Key insights or realizations +- Concrete next steps or outcomes +- Core dynamics when they shaped the outcome: arguments or reasoning that influenced decisions, tensions between competing priorities, conflicts that needed resolution, or agreements reached on contentious points +- Meaningful disagreements or trade-offs discussed + +## What to Skip + +- Detailed play-by-play of the discussion +- Implementation specifics unless they're the main point +- Process mechanics (who said what, how decisions were reached) +- Obvious context or background +- Minor disagreements that didn't impact outcomes + +## Style Guidelines + +- Use commas, parentheses, or "but" instead of em dashes +- Write in direct, conversational prose +- Prefer shorter sentences over complex punctuation +- Use "but" or "however" to show contrast +- Use parentheses for asides or clarifications +- Never use bold, italics, or other text formatting + +## Format + +- 2-4 sentences for simple conversations +- 1-2 concise paragraphs for moderate complexity +- Natural prose without bullets, headers, lists, or formatting +- Only use structure if genuinely necessary for a multi-faceted discussion + +Write as if explaining to a colleague what happened in 30 seconds. diff --git a/echo/server/prompt_templates/generate_conversation_summary.es.jinja b/echo/server/prompt_templates/generate_conversation_summary.es.jinja index 0accb441..d00aca67 100644 --- a/echo/server/prompt_templates/generate_conversation_summary.es.jinja +++ b/echo/server/prompt_templates/generate_conversation_summary.es.jinja @@ -4,54 +4,83 @@ Se le proporciona una serie de citas de una transcripción de conversación en o {{ quote_text_joined }} -Cree un resumen que se adapte a la complejidad del contenido mientras captura tanto la sustancia como los matices. +{% if project_context %} + +El anfitrión ha proporcionado el siguiente contexto y términos clave para esta conversación: +{{ project_context }} + +{% endif %} -## Directrices Principales +{% if verified_artifacts %} + +Durante esta conversación, los participantes crearon y verificaron colaborativamente los siguientes resultados concretos: +{% for artifact in verified_artifacts %} -1. **Respuesta Proporcional**: La longitud del resumen nunca debe exceder la longitud del contenido original. Para entradas muy simples, proporcione un resumen aún más corto. +--- +{{ artifact }} +--- +{% endfor %} + +{% endif %} -2. **Estructura Basada en Contenido**: - - Para intercambios simples (explicaciones, bromas, aclaraciones): Use 1-2 párrafos concisos sin secciones formales - - Para discusiones complejas: Use encabezados y estructura apropiados +Cree un resumen que capture la esencia y el significado de lo que sucedió, optimizado para una lectura rápida. -3. **Esencia Sobre Análisis**: Capture lo que realmente se dijo sin agregar contexto especulativo, antecedentes o implicaciones a menos que se mencionen explícitamente. +## Principios Fundamentales -4. **Elementos Humanos**: Preserve el humor, juegos de palabras o contexto emocional cuando sean centrales para el significado de la conversación. +1. **Esencia sobre exhaustividad**: Capture el "¿y qué?" - el resultado significativo, la decisión o el insight - no un relato detallado -5. **Profundidad Adaptable**: - - El contenido simple recibe resúmenes simples - - El contenido complejo recibe análisis estructurado apropiado - - Nunca infle contenido básico con marcos académicos o estratégicos innecesarios +2. **Concisión radical**: La mayoría de las conversaciones deberían resumirse en 2-4 oraciones. Las discusiones complejas pueden justificar 1-2 párrafos cortos. El resumen siempre debe ser considerablemente más corto que el original. -## Requisitos de Formato +3. **Compresión significativa**: Pregunte "¿qué necesitaría saber alguien si se perdió esto?" no "¿qué se discutió?" -- Para entradas muy simples (menos de 100 palabras), limite el resumen a 1-3 oraciones -- Para entradas moderadas, use 1-2 párrafos concisos -- Use encabezados de sección solo para conversaciones genuinamente complejas y multi-tema -- Resalte en negrita solo puntos genuinamente significativos, no observaciones rutinarias -- Presente como un resumen acabado y accesible que se sostiene por sí mismo +4. **Lenguaje natural**: Escriba en prosa fluida, no en viñetas o secciones estructuradas. Evite encabezados, listas y formato a menos que el contenido sea genuinamente complejo y multifacético. -Recuerde que la resumificación efectiva a menudo significa una reducción significativa en longitud mientras se preserva el significado central. +5. **Integración de contexto**: + - Integre naturalmente el contexto del proyecto cuando añada significado + - Note los artefactos verificados como resultados concretos sin detalles del proceso + - No fuerce contexto irrelevante -## Arquitectura del Resumen -- **Contexto Estratégico**: Identifique el tipo de conversación, las partes interesadas clave y los objetivos subyacentes más allá de la agenda establecida. -- **Jerarquía de Perspectivas**: Priorice las perspectivas por su potencial transformador en lugar del tiempo de discusión asignado. -- **Trayectoria de Decisiones**: Documente no solo las conclusiones sino también el camino de pensamiento crítico que llevó a ellas, especialmente las decisiones que desafían enfoques convencionales. -- **Implicaciones Estratégicas**: Conecte los puntos de discusión con objetivos organizativos más amplios o tendencias de la industria. -- **Puente de Perspectiva a Acción**: Transforme conceptos abstractos en pasos concretos con responsabilidad clara. -- **Puntos de Tensión Creativa**: Destaque los desacuerdos productivos que generaron pensamiento innovador. +6. **Puntuación de lectura rápida**: Antes de escribir, evalúe internamente qué tan fácil de escanear será este resumen en una escala de 1-10: + - 10 = Comprensión instantánea (simple, un punto claro único) + - 7-9 = Escaneo muy rápido (2-3 conclusiones claras) + - 4-6 = Necesita lectura enfocada (múltiples puntos o complejidad moderada) + - 1-3 = Requiere lectura cuidadosa (denso, complejo o matizado) + + Apunte a 7+ siempre que sea posible. Si su borrador puntúa por debajo de 7, simplifique más. -## Guía de Formato y Estilo -- Elabore una narrativa cohesiva que fluya lógicamente entre secciones -- Utilice párrafos para desarrollar ideas complejas en lugar de depender de listas excesivas -- Reserve las listas solo para elementos de acción o cuando presente múltiples opciones distintas -- Resalte las perspectivas transformadoras con impacto sistémico potencial en su flujo narrativo -- Incluya una sección "Implicaciones Estratégicas" orientada al futuro escrita en forma de párrafo -- Equilibre detalles tácticos con visión estratégica a través de prosa reflexiva -- Cuando sea apropiado, cree encabezados de sección para organizar el contenido en lugar de usar listas por defecto -- Varíe la estructura de oraciones y la longitud de párrafos para mantener el compromiso del lector -- Considere usar ocasionalmente preguntas retóricas para enmarcar perspectivas clave -- La longitud debe ser proporcional a la complejidad de la conversación (típicamente 10-15% de la longitud original) -- No exceda 3-4 párrafos +## Qué Capturar -Responda en español, independientemente del idioma de los mensajes anteriores. Puede usar formato markdown con moderación para mejorar la legibilidad, pero priorice la coherencia narrativa y los párrafos sobre elementos estructurales. \ No newline at end of file +Enfóquese en lo más importante. Incluya estos elementos cuando sean centrales para la conversación: + +- Decisiones tomadas y su justificación +- Problemas identificados y soluciones acordadas +- Insights o realizaciones clave +- Próximos pasos concretos o resultados +- Dinámicas centrales cuando moldearon el resultado: argumentos o razonamientos que influyeron en las decisiones, tensiones entre prioridades competidoras, conflictos que necesitaban resolución, o acuerdos alcanzados en puntos contenciosos +- Desacuerdos significativos o compromisos discutidos + +## Qué Omitir + +- Relato detallado de la discusión +- Especificidades de implementación a menos que sean el punto principal +- Mecánicas del proceso (quién dijo qué, cómo se tomaron las decisiones) +- Contexto o antecedentes obvios +- Desacuerdos menores que no impactaron los resultados + +## Directrices de Estilo + +- Use comas, paréntesis o "pero" en lugar de guiones largos +- Escriba en prosa directa y conversacional +- Prefiera oraciones más cortas sobre puntuación compleja +- Use "pero" o "sin embargo" para mostrar contraste +- Use paréntesis para acotaciones o aclaraciones +- Nunca use negritas, cursivas u otro formato de texto + +## Formato + +- 2-4 oraciones para conversaciones simples +- 1-2 párrafos concisos para complejidad moderada +- Prosa natural sin viñetas, encabezados, listas o formato +- Use estructura solo si es genuinamente necesario para una discusión multifacética + +Escriba como si estuviera explicando a un colega lo que sucedió en 30 segundos. diff --git a/echo/server/prompt_templates/generate_conversation_summary.fr.jinja b/echo/server/prompt_templates/generate_conversation_summary.fr.jinja index 5eb2a180..15f2d424 100644 --- a/echo/server/prompt_templates/generate_conversation_summary.fr.jinja +++ b/echo/server/prompt_templates/generate_conversation_summary.fr.jinja @@ -4,54 +4,83 @@ Vous recevez une série de citations d'une transcription de conversation dans l' {{ quote_text_joined }} -Créez un résumé qui s'adapte à la complexité du contenu tout en capturant à la fois la substance et les nuances. +{% if project_context %} + +L'hôte a fourni le contexte et les termes clés suivants pour cette conversation : +{{ project_context }} + +{% endif %} -## Directives Principales +{% if verified_artifacts %} + +Au cours de cette conversation, les participants ont créé et vérifié ensemble les résultats concrets suivants : +{% for artifact in verified_artifacts %} -1. **Réponse Proportionnelle** : La longueur du résumé ne doit jamais dépasser la longueur du contenu original. Pour les entrées très simples, fournissez un résumé encore plus court. +--- +{{ artifact }} +--- +{% endfor %} + +{% endif %} -2. **Structure Axée sur le Contenu** : - - Pour les échanges simples (explications, blagues, clarifications) : Utilisez 1-2 paragraphes concis sans sections formelles - - Pour les discussions complexes : Utilisez des en-têtes et une structure appropriés +Créez un résumé qui capture l'essence et la signification de ce qui s'est passé, optimisé pour une lecture rapide. -3. **Essence sur l'Analyse** : Capturez ce qui a été réellement dit sans ajouter de contexte spéculatif, d'antécédents ou d'implications sauf si explicitement mentionnés. +## Principes Fondamentaux -4. **Éléments Humains** : Préservez l'humour, les jeux de mots ou le contexte émotionnel lorsqu'ils sont centraux pour le sens de la conversation. +1. **L'essentiel plutôt que l'exhaustivité** : Capturez le "et alors ?" - le résultat significatif, la décision ou l'insight - pas un compte-rendu détaillé -5. **Profondeur Adaptable** : - - Le contenu simple reçoit des résumés simples - - Le contenu complexe reçoit une analyse structurée appropriée - - Ne surchargez jamais un contenu basique avec des cadres académiques ou stratégiques inutiles +2. **Concision radicale** : La plupart des conversations devraient se résumer en 2-4 phrases. Les discussions complexes peuvent justifier 1-2 courts paragraphes. Le résumé doit toujours être considérablement plus court que l'original. -## Exigences de Format +3. **Compression significative** : Demandez-vous "que devrait savoir quelqu'un qui a manqué cela ?" et non "qu'est-ce qui a été discuté ?" -- Pour les entrées très simples (moins de 100 mots), limitez le résumé à 1-3 phrases -- Pour les entrées modérées, utilisez 1-2 paragraphes concis -- Utilisez des en-têtes de section uniquement pour les conversations vraiment complexes et multi-sujets -- Mettez en gras uniquement les points vraiment significatifs, pas les observations routinières -- Présentez comme un résumé achevé et accessible qui se tient par lui-même +4. **Langage naturel** : Écrivez en prose fluide, pas en points ou sections structurées. Évitez les en-têtes, listes et formatage à moins que le contenu soit véritablement complexe et multifacette. -Rappelez-vous que la résumification efficace signifie souvent une réduction significative de la longueur tout en préservant le sens central. +5. **Intégration du contexte** : + - Intégrez naturellement le contexte du projet quand il ajoute du sens + - Notez les artefacts vérifiés comme résultats concrets sans détails de processus + - N'imposez pas un contexte non pertinent -## Architecture du Résumé -- **Contexte Stratégique** : Identifiez le type de conversation, les parties prenantes clés et les objectifs sous-jacents au-delà de l'ordre du jour énoncé. -- **Hiérarchie des Perspectives** : Priorisez les perspectives selon leur potentiel transformateur plutôt que le temps de discussion alloué. -- **Trajectoire Décisionnelle** : Documentez non seulement les conclusions mais aussi le cheminement de pensée critique qui y a mené, en particulier les décisions qui remettent en question les approches conventionnelles. -- **Implications Stratégiques** : Reliez les points de discussion aux objectifs organisationnels plus larges ou aux tendances du secteur. -- **Pont Perspective-Action** : Transformez les concepts abstraits en étapes concrètes avec une responsabilité claire. -- **Points de Tension Créative** : Mettez en évidence les désaccords productifs qui ont généré une pensée innovante. +6. **Score de lecture rapide** : Avant d'écrire, évaluez intérieurement la facilité de lecture de ce résumé sur une échelle de 1-10 : + - 10 = Compréhension instantanée (simple, un point clair unique) + - 7-9 = Scan très rapide (2-3 points clés clairs) + - 4-6 = Nécessite une lecture concentrée (plusieurs points ou complexité modérée) + - 1-3 = Nécessite une lecture attentive (dense, complexe ou nuancé) + + Visez 7+ autant que possible. Si votre brouillon obtient moins de 7, simplifiez davantage. -## Guide de Format et de Style -- Élaborez une narration cohérente qui s'écoule logiquement entre les sections -- Utilisez des paragraphes pour développer des idées complexes plutôt que de dépendre de listes excessives -- Réservez les listes uniquement aux éléments d'action ou lors de la présentation de plusieurs options distinctes -- Mettez en évidence les perspectives transformatrices avec un impact systémique potentiel dans votre flux narratif -- Incluez une section "Implications Stratégiques" orientée vers l'avenir rédigée sous forme de paragraphe -- Équilibrez les détails tactiques avec la vision stratégique à travers une prose réfléchie -- Créez des en-têtes de section pour organiser le contenu lorsque c'est approprié plutôt que de recourir à des listes par défaut -- Variez la structure des phrases et la longueur des paragraphes pour maintenir l'engagement du lecteur -- Envisagez d'utiliser occasionnellement des questions rhétoriques pour encadrer les perspectives clés -- La longueur doit être proportionnelle à la complexité de la conversation (typiquement 10-15% de la longueur originale) -- Ne dépassez pas 3-4 paragraphes +## Ce Qu'il Faut Capturer -Répondez en français, indépendamment de la langue des messages précédents. Vous pouvez utiliser le formatage markdown avec modération pour améliorer la lisibilité, mais privilégiez la cohérence narrative et les paragraphes par rapport aux éléments structurels. \ No newline at end of file +Concentrez-vous sur ce qui compte le plus. Incluez ces éléments quand ils sont centraux à la conversation : + +- Décisions prises et leur justification +- Problèmes identifiés et solutions convenues +- Insights ou réalisations clés +- Prochaines étapes concrètes ou résultats +- Dynamiques centrales quand elles ont façonné le résultat : arguments ou raisonnements qui ont influencé les décisions, tensions entre priorités concurrentes, conflits qui nécessitaient une résolution, ou accords atteints sur des points litigieux +- Désaccords significatifs ou compromis discutés + +## Ce Qu'il Faut Omettre + +- Compte-rendu détaillé de la discussion +- Spécificités d'implémentation sauf si elles sont le point principal +- Mécaniques de processus (qui a dit quoi, comment les décisions ont été prises) +- Contexte ou arrière-plan évidents +- Désaccords mineurs qui n'ont pas impacté les résultats + +## Directives de Style + +- Utilisez des virgules, parenthèses ou "mais" au lieu de tirets +- Écrivez en prose directe et conversationnelle +- Préférez les phrases courtes à la ponctuation complexe +- Utilisez "mais" ou "cependant" pour montrer le contraste +- Utilisez les parenthèses pour les apartés ou clarifications +- N'utilisez jamais le gras, l'italique ou autre formatage de texte + +## Format + +- 2-4 phrases pour les conversations simples +- 1-2 paragraphes concis pour une complexité modérée +- Prose naturelle sans puces, en-têtes, listes ou formatage +- N'utilisez la structure que si vraiment nécessaire pour une discussion multifacette + +Écrivez comme si vous expliquiez à un collègue ce qui s'est passé en 30 secondes. diff --git a/echo/server/prompt_templates/generate_conversation_summary.nl.jinja b/echo/server/prompt_templates/generate_conversation_summary.nl.jinja index 8883bcca..cfb5652a 100644 --- a/echo/server/prompt_templates/generate_conversation_summary.nl.jinja +++ b/echo/server/prompt_templates/generate_conversation_summary.nl.jinja @@ -4,33 +4,83 @@ U krijgt een reeks citaten uit een gesprekstranscript in chronologische volgorde {{ quote_text_joined }} -Maak een samenvatting die zich aanpast aan de complexiteit van de inhoud terwijl zowel de kern als de nuance worden vastgelegd. +{% if project_context %} + +De gastheer heeft de volgende context en sleuteltermen voor dit gesprek gegeven: +{{ project_context }} + +{% endif %} -## Kernrichtlijnen +{% if verified_artifacts %} + +Tijdens dit gesprek hebben deelnemers gezamenlijk de volgende concrete resultaten gecreëerd en geverifieerd: +{% for artifact in verified_artifacts %} -1. **Proportionele Reactie**: De lengte van de samenvatting mag nooit de lengte van de originele inhoud overschrijden. Voor zeer eenvoudige inputs, geef een nog kortere samenvatting. +--- +{{ artifact }} +--- +{% endfor %} + +{% endif %} -2. **Inhoud-Gedreven Structuur**: - - Voor eenvoudige uitwisselingen (uitleg, grappen, verduidelijkingen): Gebruik 1-2 beknopte paragrafen zonder formele secties - - Voor complexe discussies: Gebruik passende koppen en structuur +Maak een samenvatting die de essentie en betekenis van wat er is gebeurd vastlegt, geoptimaliseerd voor snelle leesbaarheid. -3. **Essentie Boven Analyse**: Leg vast wat er daadwerkelijk werd gezegd zonder speculatieve context, achtergrond of implicaties toe te voegen tenzij expliciet vermeld. +## Kernprincipes -4. **Menselijke Elementen**: Behoud humor, woordspelingen of emotionele context wanneer deze centraal staan in de betekenis van het gesprek. +1. **Essentie boven volledigheid**: Leg de "en dus?" vast - de betekenisvolle uitkomst, beslissing of inzicht - niet een gedetailleerd verslag -5. **Aanpasbare Diepte**: - - Eenvoudige inhoud krijgt eenvoudige samenvattingen - - Complexe inhoud krijgt een passend gestructureerde analyse - - Verzwaar eenvoudige inhoud nooit met onnodige academische of strategische kaders +2. **Radicale beknoptheid**: De meeste gesprekken zouden samengevat moeten worden in 2-4 zinnen. Complexe discussies kunnen 1-2 korte paragrafen rechtvaardigen. De samenvatting moet altijd dramatisch korter zijn dan het origineel. -## Opmaakvereisten +3. **Betekenisvolle compressie**: Vraag "wat zou iemand moeten weten als ze dit hebben gemist?" niet "wat werd er besproken?" -- Voor zeer eenvoudige inputs (onder 100 woorden), beperk de samenvatting tot 1-3 zinnen -- Voor gemiddelde inputs, gebruik 1-2 beknopte paragrafen -- Gebruik alleen sectiekoppen voor echt complexe, multi-onderwerp gesprekken -- Maak alleen echt significante punten vet, geen routine-observaties -- Presenteer als een afgeronde, toegankelijke samenvatting die op zichzelf staat +4. **Natuurlijke taal**: Schrijf in vloeiende proza, niet in opsommingstekens of gestructureerde secties. Vermijd koppen, lijsten en opmaak tenzij de inhoud echt complex en veelzijdig is. -Onthoud dat effectieve samenvatting vaak betekent: significante verkorting in lengte terwijl de kernbetekenis behouden blijft. +5. **Context integratie**: + - Verwerk projectcontext op natuurlijke wijze wanneer het betekenis toevoegt + - Vermeld geverifieerde artefacten als concrete resultaten zonder procesdetails + - Forceer geen irrelevante context -Reageer in het Nederlands, ongeacht de taal van eerdere berichten. U kunt markdown-opmaak met mate gebruiken om leesbaarheid te verbeteren, maar prioriteer verhaalcoherentie en paragrafen boven structurele elementen. \ No newline at end of file +6. **Snelle leesscore**: Beoordeel voordat je schrijft intern hoe snel deze samenvatting te scannen is op een schaal van 1-10: + - 10 = Onmiddellijk begrip (eenvoudig, één duidelijk punt) + - 7-9 = Zeer snelle scan (2-3 duidelijke conclusies) + - 4-6 = Vereist gefocust lezen (meerdere punten of matige complexiteit) + - 1-3 = Vereist zorgvuldig lezen (dicht, complex of genuanceerd) + + Streef naar 7+ waar mogelijk. Als je concept onder 7 scoort, vereenvoudig verder. + +## Wat Vast te Leggen + +Focus op wat het meest belangrijk is. Neem deze elementen op wanneer ze centraal staan in het gesprek: + +- Genomen beslissingen en hun onderbouwing +- Geïdentificeerde problemen en overeengekomen oplossingen +- Belangrijke inzichten of realisaties +- Concrete vervolgstappen of uitkomsten +- Kerndynamiek wanneer deze de uitkomst heeft gevormd: argumenten of redeneringen die beslissingen beïnvloedden, spanningen tussen concurrerende prioriteiten, conflicten die opgelost moesten worden, of bereikte overeenkomsten over betwiste punten +- Betekenisvolle meningsverschillen of besproken afwegingen + +## Wat Over te Slaan + +- Gedetailleerd verslag van de discussie +- Implementatiedetails tenzij ze het hoofdpunt zijn +- Procesmechanica (wie wat zei, hoe beslissingen werden bereikt) +- Vanzelfsprekende context of achtergrond +- Kleine meningsverschillen die geen impact hadden op uitkomsten + +## Stijlrichtlijnen + +- Gebruik komma's, haakjes of "maar" in plaats van gedachtestreepjes +- Schrijf in directe, conversationele proza +- Geef de voorkeur aan kortere zinnen boven complexe interpunctie +- Gebruik "maar" of "echter" om contrast te tonen +- Gebruik haakjes voor terzijdes of verduidelijkingen +- Gebruik nooit vetgedrukt, cursief of andere tekstopmaak + +## Formaat + +- 2-4 zinnen voor eenvoudige gesprekken +- 1-2 beknopte paragrafen voor matige complexiteit +- Natuurlijke proza zonder opsommingstekens, koppen, lijsten of opmaak +- Gebruik alleen structuur als het echt noodzakelijk is voor een veelzijdige discussie + +Schrijf alsof je aan een collega uitlegt wat er is gebeurd in 30 seconden. From 2678c2e69ad78eb91707bb3d787de3ec95d09f30 Mon Sep 17 00:00:00 2001 From: Sameer Pashikanti Date: Wed, 26 Nov 2025 17:26:29 +0000 Subject: [PATCH 3/5] enable progressive enhancement on transcription --- .../project/ProjectPortalEditor.tsx | 10 --------- echo/server/dembrane/transcribe.py | 22 ++++++++++++++++++- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/echo/frontend/src/components/project/ProjectPortalEditor.tsx b/echo/frontend/src/components/project/ProjectPortalEditor.tsx index 84a0e3d2..9099d56e 100644 --- a/echo/frontend/src/components/project/ProjectPortalEditor.tsx +++ b/echo/frontend/src/components/project/ProjectPortalEditor.tsx @@ -68,8 +68,6 @@ const normalizeTopicList = (topics: string[]): string[] => new Set(topics.map((topic) => topic.trim()).filter(Boolean)), ).sort(); -const ASSEMBLYAI_MAX_HOTWORDS = 40; - const ProperNounInput = ({ value, onChange, @@ -145,14 +143,6 @@ const ProperNounInput = ({ ))} - {nouns.length > ASSEMBLYAI_MAX_HOTWORDS && ( - - - Warning: You have added {nouns.length} key terms. Only the first{" "} - {ASSEMBLYAI_MAX_HOTWORDS} will be used by the transcription engine. - - - )} ); }; diff --git a/echo/server/dembrane/transcribe.py b/echo/server/dembrane/transcribe.py index 8df7f360..f75da56c 100644 --- a/echo/server/dembrane/transcribe.py +++ b/echo/server/dembrane/transcribe.py @@ -13,10 +13,11 @@ import logging import mimetypes from base64 import b64encode -from typing import Any, List, Literal, Optional +from typing import Any, List, Literal, Callable, Optional import litellm import requests +import sentry_sdk from dembrane.s3 import get_signed_url, get_stream_from_s3 from dembrane.llms import MODELS, get_completion_kwargs @@ -261,6 +262,9 @@ def transcribe_audio_dembrane_25_09( hotwords: Optional[List[str]] = None, use_pii_redaction: bool = False, custom_guidance_prompt: Optional[str] = None, + # the other option was to pass in a conversation_chunk_id and save the transcript there + # didn't want to leak conversation_chunk_id implementation details here + on_assemblyai_response: Callable[[str, dict[str, Any]], None] = lambda _, __: None, ) -> tuple[str, dict[str, Any]]: """Transcribe audio through custom Dembrane-25-09 workflow @@ -285,6 +289,14 @@ def transcribe_audio_dembrane_25_09( ) transcript, response = "[Nothing to transcribe]", {} + try: + if not assemblyai_response_failed and bool(on_assemblyai_response): + logger.debug("calling on_assemblyai_response") + on_assemblyai_response(transcript, response) + except Exception as e: + logger.error(f"Error in on_assemblyai_response: {e}") + sentry_sdk.capture_exception(e) + # use correction workflow to correct keyterms and fix missing segments note = None try: @@ -425,6 +437,14 @@ def transcribe_conversation_chunk( language=language, hotwords=hotwords, use_pii_redaction=use_pii_redaction, + on_assemblyai_response=lambda transcript, response: _save_transcript( + conversation_chunk_id, + transcript, + diarization={ + "schema": "Dembrane-25-09-assemblyai-partial", + "data": response, + }, + ), ) _save_transcript( conversation_chunk_id, From fbb07d858a2e3db3a977458d07e8f00363907c55 Mon Sep 17 00:00:00 2001 From: Sameer Pashikanti Date: Wed, 26 Nov 2025 17:33:40 +0000 Subject: [PATCH 4/5] Merge branch 'main' of https://github.com/Dembrane/echo into fix-permissions-ECHO-572 --- .../src/components/chat/TemplatesModal.tsx | 2 +- .../conversation/ConversationAccordion.tsx | 10 +- .../conversation/ConversationEdit.tsx | 3 + .../conversation/MoveConversationButton.tsx | 2 +- .../conversation/RetranscribeConversation.tsx | 2 +- .../conversation/VerifiedArtefactsSection.tsx | 35 +- .../dropzone/UploadConversationDropzone.tsx | 2 +- .../components/layout/ParticipantHeader.tsx | 2 +- .../components/participant/EchoErrorAlert.tsx | 8 +- .../ParticipantConversationAudio.tsx | 4 +- .../ParticipantConversationAudioContent.tsx | 1 - .../participant/refine/RefineSelection.tsx | 4 +- .../verify/VerifiedArtefactItem.tsx | 2 +- .../participant/verify/VerifyArtefact.tsx | 76 +- .../verify/VerifyArtefactError.tsx | 8 +- .../verify/VerifyArtefactLoading.tsx | 4 +- .../participant/verify/VerifyInstructions.tsx | 18 +- .../participant/verify/VerifySelection.tsx | 6 +- .../project/ProjectPortalEditor.tsx | 704 ++++++++++-------- .../components/project/ProjectTagsInput.tsx | 43 +- .../routes/project/chat/ProjectChatRoute.tsx | 4 +- .../ProjectConversationOverview.tsx | 9 +- .../project/report/ProjectReportRoute.tsx | 2 +- echo/server/dembrane/reply_utils.py | 163 ++-- .../generate_artifact.en.jinja | 2 +- .../get_reply_system.de.jinja | 17 +- .../get_reply_system.en.jinja | 15 +- .../get_reply_system.es.jinja | 17 +- .../get_reply_system.fr.jinja | 17 +- .../get_reply_system.nl.jinja | 17 +- 30 files changed, 613 insertions(+), 586 deletions(-) diff --git a/echo/frontend/src/components/chat/TemplatesModal.tsx b/echo/frontend/src/components/chat/TemplatesModal.tsx index 30ec6576..46ce57ca 100644 --- a/echo/frontend/src/components/chat/TemplatesModal.tsx +++ b/echo/frontend/src/components/chat/TemplatesModal.tsx @@ -115,7 +115,7 @@ export const TemplatesModal = ({ - Want to add a template to ECHO?{" "} + Want to add a template to "Dembrane"?{" "} + {text} ); @@ -1066,6 +1071,9 @@ export const ConversationAccordion = ({ key={tagId} size="sm" withRemoveButton + classNames={{ + root: "!bg-[var(--mantine-primary-color-light)] !font-medium", + }} onRemove={() => setSelectedTagIds((prev) => prev.filter((id) => id !== tagId), diff --git a/echo/frontend/src/components/conversation/ConversationEdit.tsx b/echo/frontend/src/components/conversation/ConversationEdit.tsx index 072295c8..4423db16 100644 --- a/echo/frontend/src/components/conversation/ConversationEdit.tsx +++ b/echo/frontend/src/components/conversation/ConversationEdit.tsx @@ -140,6 +140,9 @@ export const ConversationEdit = ({ isDirty={!!formState.dirtyFields.tagIdList} /> } + classNames={{ + pill: "!bg-[var(--mantine-primary-color-light)] text-black font-medium", + }} data={projectTags .filter((tag) => tag && tag.id != null && tag.text != null) .map((tag) => ({ diff --git a/echo/frontend/src/components/conversation/MoveConversationButton.tsx b/echo/frontend/src/components/conversation/MoveConversationButton.tsx index 23c34c43..0ca0cfb8 100644 --- a/echo/frontend/src/components/conversation/MoveConversationButton.tsx +++ b/echo/frontend/src/components/conversation/MoveConversationButton.tsx @@ -129,7 +129,7 @@ export const MoveConversationButton = ({ > - Experimental + Beta Move to Another Project diff --git a/echo/frontend/src/components/conversation/RetranscribeConversation.tsx b/echo/frontend/src/components/conversation/RetranscribeConversation.tsx index f983dce2..96d6b7c1 100644 --- a/echo/frontend/src/components/conversation/RetranscribeConversation.tsx +++ b/echo/frontend/src/components/conversation/RetranscribeConversation.tsx @@ -108,7 +108,7 @@ export const RetranscribeConversationModal = ({ {t`Retranscribe Conversation`} - Experimental + Beta } diff --git a/echo/frontend/src/components/conversation/VerifiedArtefactsSection.tsx b/echo/frontend/src/components/conversation/VerifiedArtefactsSection.tsx index 4f035bc3..3890078b 100644 --- a/echo/frontend/src/components/conversation/VerifiedArtefactsSection.tsx +++ b/echo/frontend/src/components/conversation/VerifiedArtefactsSection.tsx @@ -13,10 +13,13 @@ import { IconRosetteDiscountCheckFilled } from "@tabler/icons-react"; import { useQuery } from "@tanstack/react-query"; import { format } from "date-fns"; import { Markdown } from "@/components/common/Markdown"; +import { useVerificationTopics } from "@/components/participant/verify/hooks"; import { getVerificationArtefacts } from "@/lib/api"; type VerifiedArtefactsSectionProps = { conversationId: string; + projectId: string; + projectLanguage?: string | null; }; const formatArtefactTime = (timestamp: string | null | undefined): string => { @@ -29,8 +32,18 @@ const formatArtefactTime = (timestamp: string | null | undefined): string => { } }; +const LANGUAGE_TO_LOCALE: Record = { + de: "de-DE", + en: "en-US", + es: "es-ES", + fr: "fr-FR", + nl: "nl-NL", +}; + export const VerifiedArtefactsSection = ({ conversationId, + projectId, + projectLanguage, }: VerifiedArtefactsSectionProps) => { // Fetch all artefacts with content for display const { data: artefacts, isLoading } = useQuery({ @@ -39,6 +52,20 @@ export const VerifiedArtefactsSection = ({ queryKey: ["verify", "conversation_artifacts", conversationId], }); + const topicsQuery = useVerificationTopics(projectId); + const locale = + LANGUAGE_TO_LOCALE[projectLanguage ?? "en"] ?? LANGUAGE_TO_LOCALE.en; + + const availableTopics = topicsQuery.data?.available_topics ?? []; + const topicLabelMap = new Map( + availableTopics.map((topic) => [ + topic.key, + topic.translations?.[locale]?.label ?? + topic.translations?.["en-US"]?.label ?? + topic.key, + ]), + ); + if (isLoading) { return ( @@ -57,12 +84,12 @@ export const VerifiedArtefactsSection = ({ - <Trans>Verified Artefacts</Trans> + <Trans>Artefacts</Trans> @@ -78,7 +105,9 @@ export const VerifiedArtefactsSection = ({ - {artefact.key || ""} + + {topicLabelMap.get(artefact.key) ?? artefact.key ?? ""} + {formattedDate && ( diff --git a/echo/frontend/src/components/dropzone/UploadConversationDropzone.tsx b/echo/frontend/src/components/dropzone/UploadConversationDropzone.tsx index 6a80951e..2dd64883 100644 --- a/echo/frontend/src/components/dropzone/UploadConversationDropzone.tsx +++ b/echo/frontend/src/components/dropzone/UploadConversationDropzone.tsx @@ -789,7 +789,7 @@ export const UploadConversationDropzone = ( } title={t`Success`} - color="green.2" + color="green" variant="light" > {t`All files were uploaded successfully.`} diff --git a/echo/frontend/src/components/layout/ParticipantHeader.tsx b/echo/frontend/src/components/layout/ParticipantHeader.tsx index 4f0c6c67..4d421aa5 100644 --- a/echo/frontend/src/components/layout/ParticipantHeader.tsx +++ b/echo/frontend/src/components/layout/ParticipantHeader.tsx @@ -76,7 +76,7 @@ export const ParticipantHeader = () => { className="rounded-full" onClick={handleCancel} > - + Cancel diff --git a/echo/frontend/src/components/participant/EchoErrorAlert.tsx b/echo/frontend/src/components/participant/EchoErrorAlert.tsx index bd2eea41..5d08461b 100644 --- a/echo/frontend/src/components/participant/EchoErrorAlert.tsx +++ b/echo/frontend/src/components/participant/EchoErrorAlert.tsx @@ -13,15 +13,15 @@ export const EchoErrorAlert = ({ error }: { error: Error }) => { > {error?.message?.includes("CONTENT_POLICY_VIOLATION") ? ( - + Sorry, we cannot process this request due to an LLM provider's content policy. ) : ( - + Something went wrong. Please try again by pressing the{" "} - ECHO button, or contact support - if the issue continues. + Go deeper button, or contact + support if the issue continues. )} diff --git a/echo/frontend/src/components/participant/ParticipantConversationAudio.tsx b/echo/frontend/src/components/participant/ParticipantConversationAudio.tsx index 621f6125..aaadbf76 100644 --- a/echo/frontend/src/components/participant/ParticipantConversationAudio.tsx +++ b/echo/frontend/src/components/participant/ParticipantConversationAudio.tsx @@ -272,14 +272,14 @@ export const ParticipantConversationAudio = () => { } if (showVerify) { return ( - + "Make it concrete" available soon ); } if (showEcho) { return ( - + "Go deeper" available soon ); diff --git a/echo/frontend/src/components/participant/ParticipantConversationAudioContent.tsx b/echo/frontend/src/components/participant/ParticipantConversationAudioContent.tsx index 7845979c..e545cdf7 100644 --- a/echo/frontend/src/components/participant/ParticipantConversationAudioContent.tsx +++ b/echo/frontend/src/components/participant/ParticipantConversationAudioContent.tsx @@ -84,7 +84,6 @@ export const ParticipantConversationAudioContent = () => { const newSearchParams = new URLSearchParams(searchParams); newSearchParams.delete("echo"); setSearchParams(newSearchParams, { replace: true }); - console.log("inside echo: ", hasEchoParam); } }, []); diff --git a/echo/frontend/src/components/participant/refine/RefineSelection.tsx b/echo/frontend/src/components/participant/refine/RefineSelection.tsx index 2638218f..327cd96c 100644 --- a/echo/frontend/src/components/participant/refine/RefineSelection.tsx +++ b/echo/frontend/src/components/participant/refine/RefineSelection.tsx @@ -1,6 +1,6 @@ import { Trans } from "@lingui/react/macro"; import { Box, Group, Progress, Stack, Text, Title } from "@mantine/core"; -import { IconArrowDownToArc, IconMessageFilled } from "@tabler/icons-react"; +import { IconArrowDownToArc, IconMessage } from "@tabler/icons-react"; import { useParams } from "react-router"; import { useParticipantProjectById } from "@/components/participant/hooks"; import { useI18nNavigate } from "@/hooks/useI18nNavigate"; @@ -59,7 +59,7 @@ export const RefineSelection = () => { className="h-full px-2 py-6 justify-center" > - + <Trans id="participant.refine.make.concrete"> Make it concrete diff --git a/echo/frontend/src/components/participant/verify/VerifiedArtefactItem.tsx b/echo/frontend/src/components/participant/verify/VerifiedArtefactItem.tsx index e72255a7..f0e5b571 100644 --- a/echo/frontend/src/components/participant/verify/VerifiedArtefactItem.tsx +++ b/echo/frontend/src/components/participant/verify/VerifiedArtefactItem.tsx @@ -39,7 +39,7 @@ export const VerifiedArtefactItem = ({ <ActionIcon variant="subtle" color="blue" - aria-label="verified artefact" + aria-label="concrete artefact" size={22} > <IconRosetteDiscountCheckFilled /> diff --git a/echo/frontend/src/components/participant/verify/VerifyArtefact.tsx b/echo/frontend/src/components/participant/verify/VerifyArtefact.tsx index 29578a16..2747c708 100644 --- a/echo/frontend/src/components/participant/verify/VerifyArtefact.tsx +++ b/echo/frontend/src/components/participant/verify/VerifyArtefact.tsx @@ -8,7 +8,6 @@ import { ScrollArea, Stack, Text, - Title, } from "@mantine/core"; import { IconPencil, IconPlayerPause, IconVolume } from "@tabler/icons-react"; import { useQueryClient } from "@tanstack/react-query"; @@ -30,14 +29,6 @@ import { import { VerifyArtefactError } from "./VerifyArtefactError"; import { VerifyArtefactLoading } from "./VerifyArtefactLoading"; -const LANGUAGE_TO_LOCALE: Record<string, string> = { - de: "de-DE", - en: "en-US", - es: "es-ES", - fr: "fr-FR", - nl: "nl-NL", -}; - const MemoizedMarkdownWYSIWYG = memo(MarkdownWYSIWYG); export const VerifyArtefact = () => { @@ -76,23 +67,8 @@ export const VerifyArtefact = () => { const artefactDateUpdated = artefactQuery.data?.date_created ?? null; const selectedOptionKey = artefactQuery.data?.key ?? null; - const projectLanguage = projectQuery.data?.language ?? "en"; - const languageLocale = - LANGUAGE_TO_LOCALE[projectLanguage] ?? LANGUAGE_TO_LOCALE.en; - - const availableTopics = topicsQuery.data?.available_topics ?? []; const selectedTopics = topicsQuery.data?.selected_topics ?? []; - const selectedTopic = availableTopics.find( - (topic) => topic.key === selectedOptionKey, - ); - - const selectedOptionLabel = - selectedTopic?.translations?.[languageLocale]?.label ?? - selectedTopic?.translations?.["en-US"]?.label ?? - selectedTopic?.key ?? - t`verified`; - // biome-ignore lint/correctness/useExhaustiveDependencies: no need for navigate function in dependency useEffect(() => { // Redirect if artifact_id is missing from URL @@ -324,12 +300,12 @@ export const VerifyArtefact = () => { </div> <Stack gap="sm" align="center"> <Text size="xl" fw={600}> - <Trans id="participant.verify.regenerating.artefact"> + <Trans id="participant.concrete.regenerating.artefact"> Regenerating the artefact </Trans> </Text> <Text size="sm" c="dimmed"> - <Trans id="participant.verify.regenerating.artefact.description"> + <Trans id="participant.concrete.regenerating.artefact.description"> This will just take a few moments </Trans> </Text> @@ -337,28 +313,22 @@ export const VerifyArtefact = () => { </Stack> ) : ( <Stack gap="md" className="py-4"> - <Group justify="space-between" align="center" wrap="nowrap"> - <Title order={4} className="font-semibold"> - <Trans id="participant.verify.artefact.title"> - Artefact: {selectedOptionLabel} - </Trans> - - {readAloudUrl && ( - - {isPlaying ? ( - - ) : ( - - )} - - )} - + {readAloudUrl && ( + + {isPlaying ? ( + + ) : ( + + )} + + )} {isEditing ? ( { className="flex-1 shadow-xl" onClick={handleCancelEdit} > - Cancel + + Cancel + ) : ( @@ -419,7 +391,7 @@ export const VerifyArtefact = () => { {reviseCooldown.isOnCooldown ? ( <>{reviseCooldown.timeRemainingSeconds}s ) : ( - + Revise )} @@ -452,7 +424,7 @@ export const VerifyArtefact = () => { !artefactContent } > - + Approve diff --git a/echo/frontend/src/components/participant/verify/VerifyArtefactError.tsx b/echo/frontend/src/components/participant/verify/VerifyArtefactError.tsx index 181cef4e..20afc373 100644 --- a/echo/frontend/src/components/participant/verify/VerifyArtefactError.tsx +++ b/echo/frontend/src/components/participant/verify/VerifyArtefactError.tsx @@ -16,12 +16,12 @@ export const VerifyArtefactError = ({ return ( - + Unable to Load Artefact - + It looks like we couldn't load this artefact. This might be a temporary issue. You can try reloading or go back to select a different topic. @@ -37,7 +37,7 @@ export const VerifyArtefactError = ({ disabled={isReloading} leftSection={!isReloading && } > - + Reload Page @@ -49,7 +49,7 @@ export const VerifyArtefactError = ({ onClick={onGoBack} disabled={isReloading} > - + Go back diff --git a/echo/frontend/src/components/participant/verify/VerifyArtefactLoading.tsx b/echo/frontend/src/components/participant/verify/VerifyArtefactLoading.tsx index d0b6c0fd..90ebee38 100644 --- a/echo/frontend/src/components/participant/verify/VerifyArtefactLoading.tsx +++ b/echo/frontend/src/components/participant/verify/VerifyArtefactLoading.tsx @@ -10,12 +10,12 @@ export const VerifyArtefactLoading = () => { - + Loading artefact - + This will just take a moment diff --git a/echo/frontend/src/components/participant/verify/VerifyInstructions.tsx b/echo/frontend/src/components/participant/verify/VerifyInstructions.tsx index fe93f9d1..7ef3dd1f 100644 --- a/echo/frontend/src/components/participant/verify/VerifyInstructions.tsx +++ b/echo/frontend/src/components/participant/verify/VerifyInstructions.tsx @@ -14,15 +14,15 @@ const INSTRUCTIONS = [ { key: "receive-artefact", render: (objectLabel: string) => ( - - You'll soon get {objectLabel} to verify. + + You'll soon get {objectLabel} to make them concrete. ), }, { key: "read-aloud", render: (objectLabel: string) => ( - + Once you receive the {objectLabel}, read it aloud and share out loud what you want to change, if anything. @@ -31,7 +31,7 @@ const INSTRUCTIONS = [ { key: "revise-artefact", render: (objectLabel: string) => ( - + Once you have discussed, hit "revise" to see the {objectLabel} change to reflect your discussion. @@ -40,7 +40,7 @@ const INSTRUCTIONS = [ { key: "approve-artefact", render: (objectLabel: string) => ( - + If you are happy with the {objectLabel} click "Approve" to show you feel heard. @@ -49,7 +49,7 @@ const INSTRUCTIONS = [ { key: "approval-helps", render: (_objectLabel: string) => ( - + Your approval helps us understand what you really think! ), @@ -89,7 +89,7 @@ export const VerifyInstructions = ({ {/* Next button */} diff --git a/echo/frontend/src/components/participant/verify/VerifySelection.tsx b/echo/frontend/src/components/participant/verify/VerifySelection.tsx index 1e0bf667..acc07643 100644 --- a/echo/frontend/src/components/participant/verify/VerifySelection.tsx +++ b/echo/frontend/src/components/participant/verify/VerifySelection.tsx @@ -180,7 +180,7 @@ export const VerifySelection = () => { {/* Main content */} - <Trans id="participant.verify.selection.title"> + <Trans id="participant.concrete.selection.title"> What do you want to make concrete? </Trans> @@ -222,7 +222,7 @@ export const VerifySelection = () => { {/* Next button */} diff --git a/echo/frontend/src/components/project/ProjectPortalEditor.tsx b/echo/frontend/src/components/project/ProjectPortalEditor.tsx index 9099d56e..8435a04c 100644 --- a/echo/frontend/src/components/project/ProjectPortalEditor.tsx +++ b/echo/frontend/src/components/project/ProjectPortalEditor.tsx @@ -2,6 +2,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { t } from "@lingui/core/macro"; import { Trans } from "@lingui/react/macro"; import { + ActionIcon, Badge, Box, Button, @@ -11,7 +12,6 @@ import { InputDescription, NativeSelect, Paper, - Pill, Stack, Switch, Text, @@ -19,7 +19,7 @@ import { TextInput, Title, } from "@mantine/core"; -import { IconEye, IconEyeOff, IconRefresh } from "@tabler/icons-react"; +import { IconEye, IconEyeOff, IconRefresh, IconX } from "@tabler/icons-react"; import { useQueryClient } from "@tanstack/react-query"; import { Resizable } from "re-resizable"; import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; @@ -134,13 +134,28 @@ const ProperNounInput = ({ /> {nouns.map((noun) => ( - handleRemoveNoun(noun)} + variant="light" + c="black" + size="lg" + style={{ + fontWeight: 500, + textTransform: "none", + }} + rightSection={ + handleRemoveNoun(noun)} + size="xs" + variant="transparent" + c="gray.8" + > + + + } > - {noun} - + {noun} + ))} @@ -519,313 +534,386 @@ const ProjectPortalEditorComponent: React.FC = ({ + + + <Trans>Participant Features</Trans> + + + + + + <Trans>Go deeper</Trans> + + + + Beta + + - - - - <Trans>Dembrane ECHO</Trans> - - - - Experimental - - - - - - Enable this feature to allow participants to request - AI-powered responses during their conversation. - Participants can click "ECHO" after recording their - thoughts to receive contextual feedback, encouraging - deeper reflection and engagement. A cooldown period - applies between requests. - - + + + Enable this feature to allow participants to request + AI-powered responses during their conversation. + Participants can click "Go deeper" after recording + their thoughts to receive contextual feedback, + encouraging deeper reflection and engagement. A + cooldown period applies between requests. + + - ( - ( + + } + checked={field.value} + onChange={(e) => + field.onChange(e.currentTarget.checked) } /> - } - checked={field.value} - onChange={(e) => - field.onChange(e.currentTarget.checked) - } + )} /> - )} - /> - - ( - - - - - Select the type of feedback or engagement you want - to encourage. - - - - - watchedReplyEnabled && field.onChange("summarize") - } - > - Summarize - - - watchedReplyEnabled && - field.onChange("brainstorm") - } - > - Brainstorm Ideas - - - watchedReplyEnabled && field.onChange("custom") - } - > - Custom - - - - )} - /> - {watchedReplyMode === "custom" && ( - ( -