From e7fb73c53ee93edf9d9e362aaafb6622943cb8b1 Mon Sep 17 00:00:00 2001 From: charles Date: Tue, 20 Jan 2026 19:27:24 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A8(backend)=20various=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I fix linters and rabbit request change --- .../albert_rag_backend.py | 4 +- .../document_rag_backends/base_rag_backend.py | 13 +- .../document_rag_backends/find_rag_backend.py | 5 +- src/backend/chat/tools/web_search_brave.py | 2 +- .../src/features/chat/components/Chat.tsx | 312 +++++++++++++++++- 5 files changed, 308 insertions(+), 28 deletions(-) diff --git a/src/backend/chat/agent_rag/document_rag_backends/albert_rag_backend.py b/src/backend/chat/agent_rag/document_rag_backends/albert_rag_backend.py index 3dc4000..045a8ef 100644 --- a/src/backend/chat/agent_rag/document_rag_backends/albert_rag_backend.py +++ b/src/backend/chat/agent_rag/document_rag_backends/albert_rag_backend.py @@ -87,7 +87,7 @@ class AlbertRagBackend(BaseRagBackend): # pylint: disable=too-many-instance-att self.collection_id = str(response.json()["id"]) return self.collection_id - def delete_collection(self) -> None: + def delete_collection(self, **kwargs) -> None: """ Delete the current collection """ @@ -98,7 +98,7 @@ class AlbertRagBackend(BaseRagBackend): # pylint: disable=too-many-instance-att ) response.raise_for_status() - async def adelete_collection(self) -> None: + async def adelete_collection(self, **kwargs) -> None: """ Asynchronously delete the current collection """ diff --git a/src/backend/chat/agent_rag/document_rag_backends/base_rag_backend.py b/src/backend/chat/agent_rag/document_rag_backends/base_rag_backend.py index 5d00ad9..6672628 100644 --- a/src/backend/chat/agent_rag/document_rag_backends/base_rag_backend.py +++ b/src/backend/chat/agent_rag/document_rag_backends/base_rag_backend.py @@ -1,6 +1,7 @@ """Implementation of the Albert API for RAG document search.""" import logging +from abc import ABC, abstractmethod from contextlib import asynccontextmanager, contextmanager from io import BytesIO from typing import List, Optional @@ -13,7 +14,7 @@ from chat.agent_rag.document_converter.parser import BaseParser logger = logging.getLogger(__name__) -class BaseRagBackend: +class BaseRagBackend(ABC): """Base class for RAG backends.""" def __init__( @@ -62,6 +63,7 @@ class BaseRagBackend: ) return collection_ids + @abstractmethod def create_collection(self, name: str, description: Optional[str] = None) -> str: """ Create a temporary collection for the search operation. @@ -92,6 +94,7 @@ class BaseRagBackend: """ return self.parser.parse_document(name, content_type, content) + @abstractmethod def store_document(self, name: str, content: str, **kwargs) -> None: """ Store the document content in the collection. @@ -135,7 +138,8 @@ class BaseRagBackend: self.store_document(name, document_content, **kwargs) return document_content - def delete_collection(self, **kwargs) -> None: + @abstractmethod + def delete_collection(self, **kwargs) -> None: """ Delete the collection. This method should handle the logic to delete the collection from the backend. @@ -149,6 +153,7 @@ class BaseRagBackend: """ return await sync_to_async(self.delete_collection)(**kwargs) + @abstractmethod def search(self, query: str, results_count: int = 4, **kwargs) -> RAGWebResults: """ Search the collection for the given query. @@ -185,7 +190,9 @@ class BaseRagBackend: @classmethod @asynccontextmanager - async def temporary_collection_async(cls, name: str, description: Optional[str] = None, **kwargs): + async def temporary_collection_async( + cls, name: str, description: Optional[str] = None, **kwargs + ): """Context manager for RAG backend with temporary collections.""" backend = cls() diff --git a/src/backend/chat/agent_rag/document_rag_backends/find_rag_backend.py b/src/backend/chat/agent_rag/document_rag_backends/find_rag_backend.py index 59a2008..2855dd2 100644 --- a/src/backend/chat/agent_rag/document_rag_backends/find_rag_backend.py +++ b/src/backend/chat/agent_rag/document_rag_backends/find_rag_backend.py @@ -61,9 +61,7 @@ class FindRagBackend(BaseRagBackend): urljoin(settings.FIND_API_URL, self.deleting_endpoint), headers={"Authorization": f"Bearer {kwargs['session'].get('oidc_access_token')}"}, json={ - "tags": [ - f"collection-{self.collection_id}" - ], + "tags": [f"collection-{self.collection_id}"], # "service": "conversations" }, timeout=settings.FIND_API_TIMEOUT, @@ -123,7 +121,6 @@ class FindRagBackend(BaseRagBackend): RAGWebResults: The search results. """ logger.debug("search documents in Find with query '%s'", query) - print("q----", query) response = requests.post( urljoin(settings.FIND_API_URL, self.search_endpoint), headers={"Authorization": f"Bearer {kwargs['session'].get('oidc_access_token')}"}, diff --git a/src/backend/chat/tools/web_search_brave.py b/src/backend/chat/tools/web_search_brave.py index 51bcae7..cd32dd4 100644 --- a/src/backend/chat/tools/web_search_brave.py +++ b/src/backend/chat/tools/web_search_brave.py @@ -326,7 +326,7 @@ async def web_search_brave_with_document_backend(ctx: RunContext, query: str) -> query=query, results_count=settings.BRAVE_RAG_WEB_SEARCH_CHUNK_NUMBER, session=ctx.deps.session, - user_sub = ctx.deps.user.sub, + user_sub=ctx.deps.user.sub, ) logger.info("RAG search returned: %s", rag_results) diff --git a/src/frontend/apps/conversations/src/features/chat/components/Chat.tsx b/src/frontend/apps/conversations/src/features/chat/components/Chat.tsx index b563a07..a620c48 100644 --- a/src/frontend/apps/conversations/src/features/chat/components/Chat.tsx +++ b/src/frontend/apps/conversations/src/features/chat/components/Chat.tsx @@ -1,4 +1,4 @@ -import { Message, SourceUIPart } from '@ai-sdk/ui-utils'; +import { Message, SourceUIPart, ToolInvocationUIPart } from '@ai-sdk/ui-utils'; import { Modal, ModalSize } from '@openfun/cunningham-react'; import 'katex/dist/katex.min.css'; // `rehype-katex` does not import the CSS for you import { useRouter } from 'next/router'; @@ -227,10 +227,6 @@ export const Chat = ({ const stopGeneration = async () => { stopChat(); - if (!conversationId) { - return; - } - const response = await fetchAPI(`chats/${conversationId}/stop-streaming/`, { method: 'POST', }); @@ -664,19 +660,299 @@ export const Chat = ({ return ( + data-message-id={message.id} + $css={` + display: flex; + width: 100%; + margin: auto; + margin-bottom: ${isLastAssistantMessageInConversation ? '30px' : '0px'}; + color: var(--c--theme--colors--greyscale-850); + padding-left: 12px; + padding-right: 12px; + max-width: 750px; + text-align: left; + overflow-wrap: anywhere; + flex-direction: ${message.role === 'user' ? 'row-reverse' : 'row'}; + `} + > + + {message.experimental_attachments && + message.experimental_attachments.length > 0 && ( + + + + )} + + {/* Message content */} + {message.content && ( + +

+ {message.role === 'user' + ? t('You said: ') + : t('Assistant IA replied: ')} +

+ {message.role === 'user' ? ( + + {message.content} + + ) : ( + ( + + ), + a: ({ children, ...props }) => ( + + {children} + + ), + // eslint-disable-next-line @typescript-eslint/no-unused-vars + pre: ({ node, children, ...props }) => ( + {children} + ), + }} + > + {message.content} + + )} +
+ )} + + + {isCurrentlyStreaming && + isLastAssistantMessageInConversation && + status === 'streaming' && + message.parts?.some( + (part) => + part.type === 'tool-invocation' && + part.toolInvocation.toolName !== + 'document_parsing', + ) && ( + + + + {(() => { + const toolInvocation = message.parts?.find( + (part) => + part.type === 'tool-invocation' && + part.toolInvocation.toolName !== + 'document_parsing', + ); + if ( + toolInvocation?.type === + 'tool-invocation' && + toolInvocation.toolInvocation.toolName === + 'summarize' + ) { + return t('Summarizing...'); + } + return t('Search...'); + })()} + + + )} + {message.parts + ?.filter((part) => part.type === 'tool-invocation') + .map( + (part: ToolInvocationUIPart, partIndex: number) => + part.type === 'tool-invocation' && + isCurrentlyStreaming && + isLastAssistantMessageInConversation ? ( + + ) : null, + )} + + {message.role === 'assistant' && + !( + isLastAssistantMessageInConversation && + status === 'streaming' + ) && ( + + + copyToClipboard(message.content)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + copyToClipboard(message.content); + } + }} + role="button" + tabIndex={0} + > + + {!isMobile && ( + + {t('Copy')} + + )} + + {message.parts?.some( + (part) => part.type === 'source', + ) && + (() => { + const sourceCount = + message.parts?.filter( + (part) => part.type === 'source', + ).length || 0; + return ( + openSources(message.id)} + onKeyDown={(e) => { + if ( + e.key === 'Enter' || + e.key === ' ' + ) { + e.preventDefault(); + openSources(message.id); + } + }} + role="button" + tabIndex={0} + > + + + {t('Show')} {sourceCount}{' '} + {sourceCount !== 1 + ? t('sources') + : t('source')} + + + ); + })()} + + + {/* We should display the button, but disabled if no trace linked */} + {conversationId && + message.id && + message.id.startsWith('trace-') && ( + + )} + + + )} + {message.parts && + isSourceOpen === message.id && + (() => { + const sourceParts = message.parts.filter( + (part): part is SourceUIPart => + part.type === 'source', + ); + return ( + + + + ); + })()} +
+
+
); })}