📝(service-public): style tools
This commit is contained in:
@@ -428,7 +428,7 @@ class AIAgentService: # pylint: disable=too-many-instance-attributes
|
||||
"If the user request relates to French public services, laws or"
|
||||
" administrative topics, you MUST call the 'service_public' tool"
|
||||
" before answering. Use it to retrieve relevant passages and then"
|
||||
" answer the user. When using this tool, end your answer with sources urls if present in the tool response."
|
||||
" answer the user."
|
||||
)
|
||||
|
||||
conversation_has_documents = self._is_document_upload_enabled and (
|
||||
@@ -691,19 +691,29 @@ class AIAgentService: # pylint: disable=too-many-instance-attributes
|
||||
if event.result.metadata and (
|
||||
sources := event.result.metadata.get("sources")
|
||||
):
|
||||
for source_url in sources:
|
||||
url_source = LanguageModelV1Source(
|
||||
sourceType="url",
|
||||
id=str(uuid.uuid4()),
|
||||
url=source_url,
|
||||
providerMetadata={},
|
||||
)
|
||||
_new_source_ui = SourceUIPart(
|
||||
type="source", source=url_source
|
||||
)
|
||||
_ui_sources.append(_new_source_ui)
|
||||
yield events_v4.SourcePart(
|
||||
**_new_source_ui.source.model_dump()
|
||||
for source_item in sources:
|
||||
# Handle both old format (string) and new format (dict)
|
||||
if isinstance(source_item, dict):
|
||||
source_url = source_item.get("url", "")
|
||||
source_title = source_item.get("title", "")
|
||||
else:
|
||||
# Fallback for old string format
|
||||
source_url = source_item
|
||||
source_title = ""
|
||||
|
||||
if source_url:
|
||||
url_source = LanguageModelV1Source(
|
||||
sourceType="url",
|
||||
id=str(uuid.uuid4()),
|
||||
url=source_url,
|
||||
providerMetadata={"title": source_title} if source_title else {},
|
||||
)
|
||||
_new_source_ui = SourceUIPart(
|
||||
type="source", source=url_source
|
||||
)
|
||||
_ui_sources.append(_new_source_ui)
|
||||
yield events_v4.SourcePart(
|
||||
**_new_source_ui.source.model_dump()
|
||||
)
|
||||
|
||||
yield events_v4.ToolResultPart(
|
||||
|
||||
@@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
# Default curated collections (Albert IDs)
|
||||
DEFAULT_COLLECTION_IDS: List[int] = [784, 785] # travail-emploi, service-public
|
||||
INSTRUCTIONS = "Voilà les informations trouvées, résume les pour répondre à la question de l'utilisateur, à la fin de ta réponse, ajoutes une section sources avec les urls des sources si présentes: \n"
|
||||
INSTRUCTIONS = "Voilà les informations trouvées, résume les pour répondre à la question de l'utilisateur si c'est pertinent: \n"
|
||||
|
||||
async def service_public(ctx: RunContext, query: str) -> ToolReturn:
|
||||
"""Search curated Service-Public collections on Albert and return snippets.
|
||||
@@ -33,20 +33,29 @@ async def service_public(ctx: RunContext, query: str) -> ToolReturn:
|
||||
# Search in the curated collections
|
||||
rag_results = backend.search(query, collections=DEFAULT_COLLECTION_IDS)
|
||||
|
||||
# Convert to compact format for the model using your logic
|
||||
# Convert to compact format for the model
|
||||
compact = []
|
||||
sources = []
|
||||
for result in rag_results.data:
|
||||
# AlbertRagBackend.search() returns RAGWebResult objects with {url, content, score, metadata}
|
||||
document_name = result.metadata.get("document_name", "Document")
|
||||
url = result.metadata.get("url", "")
|
||||
|
||||
compact.append(
|
||||
{
|
||||
"title": result.metadata["document_name"],
|
||||
"title": document_name,
|
||||
"snippet": result.content,
|
||||
"url": result.metadata["url"],
|
||||
"url": url,
|
||||
}
|
||||
)
|
||||
if result.metadata["url"]:
|
||||
sources.append(result.metadata["url"])
|
||||
|
||||
# Create rich source with title and URL
|
||||
if url:
|
||||
source_info = {
|
||||
"title": document_name,
|
||||
"url": url
|
||||
}
|
||||
sources.append(source_info)
|
||||
|
||||
# Update run usage
|
||||
ctx.usage += RunUsage(
|
||||
@@ -54,10 +63,18 @@ async def service_public(ctx: RunContext, query: str) -> ToolReturn:
|
||||
output_tokens=rag_results.usage.completion_tokens,
|
||||
)
|
||||
|
||||
# Remove duplicate sources based on URL
|
||||
unique_sources = []
|
||||
seen_urls = set()
|
||||
for source in sources:
|
||||
if source["url"] not in seen_urls:
|
||||
unique_sources.append(source)
|
||||
seen_urls.add(source["url"])
|
||||
|
||||
return ToolReturn(
|
||||
return_value=INSTRUCTIONS + str(compact),
|
||||
content='',
|
||||
metadata={"sources": list(set(sources))},
|
||||
metadata={"sources": unique_sources},
|
||||
)
|
||||
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
|
||||
@@ -659,7 +659,7 @@ export const Chat = ({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
p: ({ node, ...props }) => (
|
||||
<Text
|
||||
$css="display: block"
|
||||
$css="display: block; white-space: pre-wrap;"
|
||||
$theme="greyscale"
|
||||
$variation="850"
|
||||
{...props}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { AttachmentList } from './AttachmentList';
|
||||
import { ModelSelector } from './ModelSelector';
|
||||
import { ScrollDown } from './ScrollDown';
|
||||
import { SendButton } from './SendButton';
|
||||
import { ToolSelector } from './ToolSelector';
|
||||
|
||||
interface InputChatProps {
|
||||
messagesLength: number;
|
||||
@@ -593,6 +594,10 @@ export const InputChat = ({
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box $padding={{ horizontal: 'xs' }}>
|
||||
<ToolSelector />
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
$direction="row"
|
||||
|
||||
@@ -179,15 +179,26 @@ export const SourceItem: React.FC<SourceItemProps> = ({ url, metadata }) => {
|
||||
>
|
||||
{renderFavicon()}
|
||||
|
||||
{new URL(url).hostname}
|
||||
|
||||
<Box
|
||||
$padding={{ right: '4px' }}
|
||||
$align="center"
|
||||
style={styles.title}
|
||||
>
|
||||
{title}
|
||||
</Box>
|
||||
{title ? (
|
||||
<Box
|
||||
$padding={{ right: '4px' }}
|
||||
$align="center"
|
||||
style={styles.title}
|
||||
>
|
||||
{title}
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
{new URL(url).hostname}
|
||||
<Box
|
||||
$padding={{ right: '4px' }}
|
||||
$align="center"
|
||||
style={styles.title}
|
||||
>
|
||||
{title}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</StyledLink>
|
||||
) : (
|
||||
<Box>{url}</Box>
|
||||
|
||||
@@ -36,13 +36,27 @@ const SourceItemListComponent: React.FC<SourceItemListProps> = ({
|
||||
overflow: hidden;
|
||||
`}
|
||||
>
|
||||
{parts.map((part) => (
|
||||
<SourceItem
|
||||
key={part.source.url}
|
||||
url={part.source.url}
|
||||
metadata={getMetadata(part.source.url)}
|
||||
/>
|
||||
))}
|
||||
{parts.map((part) => {
|
||||
const metadata = getMetadata(part.source.url);
|
||||
// Extract title from providerMetadata if available
|
||||
const providerTitle = part.source.providerMetadata?.title;
|
||||
|
||||
return (
|
||||
<SourceItem
|
||||
key={part.source.url}
|
||||
url={part.source.url}
|
||||
metadata={metadata ? {
|
||||
...metadata,
|
||||
title: providerTitle || metadata.title
|
||||
} : providerTitle ? {
|
||||
title: providerTitle,
|
||||
favicon: null,
|
||||
loading: false,
|
||||
error: false
|
||||
} : undefined}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
import { Button } from '@openfun/cunningham-react';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, Icon, Text } from '@/components';
|
||||
import { useChatPreferencesStore } from '@/features/chat/stores/useChatPreferencesStore';
|
||||
|
||||
interface ToolSelectorProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// Define available tools with their display names
|
||||
const AVAILABLE_TOOLS = [
|
||||
{
|
||||
id: 'service_public',
|
||||
name: 'Service Public',
|
||||
icon: 'public',
|
||||
},
|
||||
];
|
||||
|
||||
export const ToolSelector = ({ className }: ToolSelectorProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { selectedTools, toggleSelectedTool } = useChatPreferencesStore();
|
||||
|
||||
const handleToolToggle = (toolId: string) => {
|
||||
toggleSelectedTool(toolId);
|
||||
};
|
||||
|
||||
const selectedToolsCount = selectedTools.length;
|
||||
const hasSelectedTools = selectedToolsCount > 0;
|
||||
|
||||
return (
|
||||
<Box
|
||||
$position="relative"
|
||||
className={className}
|
||||
$css={`
|
||||
display: inline-block;
|
||||
z-index: ${isOpen ? 1000 : 'auto'};
|
||||
`}
|
||||
>
|
||||
<Box
|
||||
$css={`
|
||||
${
|
||||
hasSelectedTools
|
||||
? `
|
||||
.tool-selector-button {
|
||||
background-color: var(--c--theme--colors--primary-100) !important;
|
||||
}
|
||||
`
|
||||
: ''
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
type="button"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
aria-label={t('More tools')}
|
||||
className="c__button--neutral tool-selector-button"
|
||||
icon={
|
||||
<Icon
|
||||
iconName="build"
|
||||
$theme="greyscale"
|
||||
$variation="550"
|
||||
$size="16px"
|
||||
$css={`
|
||||
color: ${hasSelectedTools ? 'var(--c--theme--colors--primary-600) !important' : 'var(--c--theme--colors--greyscale-600)'}
|
||||
`}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Text
|
||||
$theme={hasSelectedTools ? 'primary' : 'greyscale'}
|
||||
$variation="550"
|
||||
>
|
||||
{hasSelectedTools ? `${selectedToolsCount} outil${selectedToolsCount > 1 ? 's' : ''}` : t('More tools')}
|
||||
</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{isOpen && (
|
||||
<>
|
||||
{/* Backdrop to close the dropdown */}
|
||||
<Box
|
||||
$position="fixed"
|
||||
$css={`
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 999;
|
||||
`}
|
||||
onClick={() => setIsOpen(false)}
|
||||
/>
|
||||
|
||||
{/* Dropdown menu */}
|
||||
<Box
|
||||
$position="absolute"
|
||||
$css={`
|
||||
bottom: 100%;
|
||||
left: 0;
|
||||
margin-bottom: 6px;
|
||||
background: white;
|
||||
border: 1px solid var(--c--theme--colors--greyscale-200);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
min-width: 160px;
|
||||
overflow: hidden;
|
||||
`}
|
||||
>
|
||||
<Box $padding={{ all: 'xs' }}>
|
||||
<Box $css="display: flex; flex-direction: column; gap: 1px;">
|
||||
{AVAILABLE_TOOLS.map((tool) => {
|
||||
const isSelected = selectedTools.includes(tool.id);
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={tool.id}
|
||||
$css={`
|
||||
display: flex;
|
||||
align-items: left;
|
||||
justify-content: space-between;
|
||||
padding: 6px 8px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
background-color: ${isSelected ? 'var(--c--theme--colors--primary-100)' : 'transparent'};
|
||||
|
||||
&:hover {
|
||||
background-color: ${isSelected ? 'var(--c--theme--colors--primary-200)' : 'var(--c--theme--colors--greyscale-100)'};
|
||||
}
|
||||
`}
|
||||
onClick={() => handleToolToggle(tool.id)}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', flex: 1 }}>
|
||||
<Icon
|
||||
iconName={tool.icon}
|
||||
$theme="greyscale"
|
||||
$variation="600"
|
||||
$size="16px"
|
||||
$css="margin-right: 8px;"
|
||||
/>
|
||||
|
||||
<span style={{
|
||||
fontSize: '12px',
|
||||
fontWeight: '500',
|
||||
color: isSelected ? 'var(--c--theme--colors--primary-600)' : 'var(--c--theme--colors--greyscale-600)',
|
||||
whiteSpace: 'nowrap'
|
||||
}}>
|
||||
{tool.name}
|
||||
</span>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user