3216b634a3
⚠️ **AI-generated PR — not ready for review** ⚠️ cc @FelixMalfait --- ## Changes ### System prompt improvements - Explicit skill-before-tools workflow to prevent the model from calling tools without loading the matching skill first - Data efficiency guidance (default small limits, use filters) - Pluralized `load_skill` → `load_skills` for consistency with `load_tools` ### Token usage reduction - Output serialization layer: strips null/undefined/empty values from tool results - Lowered default `find_*` limit from 100 → 10, max from 1000 → 100 ### System object tool generation - System objects (calendar events, messages, etc.) now generate AI tools - Only workflow-related and favorite-related objects are excluded ### Context window display fix - **Bug**: UI compared cumulative tokens (sum of all turns) against single-request context window → showed 100% after a few turns - **Fix**: Track `conversationSize` (last step's `inputTokens`) which represents the actual conversation history size sent to the model - New `conversationSize` column on thread entity with migration ### Workspace AI instructions - Support for custom workspace-level AI instructions --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
249 lines
7.7 KiB
TypeScript
249 lines
7.7 KiB
TypeScript
import { useTheme } from '@emotion/react';
|
|
import styled from '@emotion/styled';
|
|
import { t } from '@lingui/core/macro';
|
|
import { useLingui } from '@lingui/react/macro';
|
|
import { useState } from 'react';
|
|
import { useRecoilValue } from 'recoil';
|
|
import { isDefined } from 'twenty-shared/utils';
|
|
import { ProgressBar } from 'twenty-ui/feedback';
|
|
|
|
import { ContextUsageProgressRing } from '@/ai/components/internal/ContextUsageProgressRing';
|
|
import {
|
|
agentChatUsageState,
|
|
type AgentChatLastMessageUsage,
|
|
} from '@/ai/states/agentChatUsageState';
|
|
|
|
const StyledContainer = styled.div`
|
|
position: relative;
|
|
`;
|
|
|
|
const StyledTrigger = styled.div<{ hasUsage: boolean }>`
|
|
align-items: center;
|
|
background: transparent;
|
|
border: 1px solid ${({ theme }) => theme.background.transparent.medium};
|
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
|
cursor: ${({ hasUsage }) => (hasUsage ? 'pointer' : 'default')};
|
|
display: flex;
|
|
gap: ${({ theme }) => theme.spacing(1)};
|
|
height: 24px;
|
|
padding: 0 ${({ theme }) => theme.spacing(2)};
|
|
transition: background 0.1s ease;
|
|
|
|
&:hover {
|
|
background: ${({ theme, hasUsage }) =>
|
|
hasUsage ? theme.background.transparent.light : 'transparent'};
|
|
}
|
|
`;
|
|
|
|
const StyledPercentage = styled.span`
|
|
color: ${({ theme }) => theme.font.color.secondary};
|
|
font-size: ${({ theme }) => theme.font.size.sm};
|
|
font-weight: ${({ theme }) => theme.font.weight.medium};
|
|
`;
|
|
|
|
const StyledHoverCard = styled.div`
|
|
background: ${({ theme }) => theme.background.primary};
|
|
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
|
border-radius: ${({ theme }) => theme.border.radius.md};
|
|
box-shadow: ${({ theme }) => theme.boxShadow.strong};
|
|
min-width: 280px;
|
|
position: absolute;
|
|
right: 0;
|
|
bottom: calc(100% + 8px);
|
|
z-index: ${({ theme }) => theme.lastLayerZIndex};
|
|
`;
|
|
|
|
const StyledSection = styled.div`
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: ${({ theme }) => theme.spacing(2)};
|
|
padding: ${({ theme }) => theme.spacing(3)};
|
|
`;
|
|
|
|
const StyledRow = styled.div`
|
|
align-items: center;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
`;
|
|
|
|
const StyledLabel = styled.span`
|
|
color: ${({ theme }) => theme.font.color.secondary};
|
|
font-size: ${({ theme }) => theme.font.size.sm};
|
|
`;
|
|
|
|
const StyledValue = styled.span`
|
|
color: ${({ theme }) => theme.font.color.tertiary};
|
|
font-size: ${({ theme }) => theme.font.size.sm};
|
|
`;
|
|
|
|
const StyledSectionTitle = styled.span`
|
|
color: ${({ theme }) => theme.font.color.primary};
|
|
font-size: ${({ theme }) => theme.font.size.xs};
|
|
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
`;
|
|
|
|
const StyledDivider = styled.div`
|
|
border-top: 1px solid ${({ theme }) => theme.border.color.light};
|
|
`;
|
|
|
|
const formatTokenCount = (count: number): string => {
|
|
if (count >= 1_000_000_000) {
|
|
return `${(count / 1_000_000_000).toFixed(1)}B`;
|
|
}
|
|
if (count >= 1_000_000) {
|
|
return `${(count / 1_000_000).toFixed(1)}M`;
|
|
}
|
|
if (count >= 1_000) {
|
|
return `${(count / 1_000).toFixed(1)}K`;
|
|
}
|
|
return count.toString();
|
|
};
|
|
|
|
const formatCredits = (credits: number): string => {
|
|
// Credits are already in display units from the API (internal / 1000)
|
|
// Show up to 1 decimal for fractional values, none for whole numbers
|
|
if (Number.isInteger(credits)) {
|
|
return credits.toLocaleString();
|
|
}
|
|
|
|
return credits.toLocaleString(undefined, {
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 1,
|
|
});
|
|
};
|
|
|
|
const getCachedLabel = (lastMessage: AgentChatLastMessageUsage): string => {
|
|
if (lastMessage.cachedInputTokens <= 0 || lastMessage.inputTokens <= 0) {
|
|
return '';
|
|
}
|
|
|
|
const cachedPercent = Math.round(
|
|
(lastMessage.cachedInputTokens / lastMessage.inputTokens) * 100,
|
|
);
|
|
|
|
return ` (${t`${cachedPercent}% cached`})`;
|
|
};
|
|
|
|
export const AIChatContextUsageButton = () => {
|
|
const { t } = useLingui();
|
|
const theme = useTheme();
|
|
const [isHovered, setIsHovered] = useState(false);
|
|
const agentChatUsage = useRecoilValue(agentChatUsageState);
|
|
|
|
if (!agentChatUsage) {
|
|
return (
|
|
<StyledContainer>
|
|
<StyledTrigger hasUsage={false}>
|
|
<ContextUsageProgressRing percentage={0} />
|
|
<StyledPercentage>0%</StyledPercentage>
|
|
</StyledTrigger>
|
|
</StyledContainer>
|
|
);
|
|
}
|
|
|
|
const percentage = Math.min(
|
|
(agentChatUsage.conversationSize / agentChatUsage.contextWindowTokens) *
|
|
100,
|
|
100,
|
|
);
|
|
const formattedPercentage = percentage.toFixed(1);
|
|
const totalCredits =
|
|
agentChatUsage.inputCredits + agentChatUsage.outputCredits;
|
|
const lastMessage = agentChatUsage.lastMessage;
|
|
|
|
return (
|
|
<StyledContainer
|
|
onMouseEnter={() => setIsHovered(true)}
|
|
onMouseLeave={() => setIsHovered(false)}
|
|
>
|
|
<StyledTrigger hasUsage={true}>
|
|
<ContextUsageProgressRing percentage={percentage} />
|
|
<StyledPercentage>{formattedPercentage}%</StyledPercentage>
|
|
</StyledTrigger>
|
|
|
|
{isHovered && (
|
|
<StyledHoverCard>
|
|
<StyledSection>
|
|
<StyledRow>
|
|
<StyledPercentage>{formattedPercentage}%</StyledPercentage>
|
|
<StyledValue>
|
|
{formatTokenCount(agentChatUsage.conversationSize)} /{' '}
|
|
{formatTokenCount(agentChatUsage.contextWindowTokens)}{' '}
|
|
{t`tokens`}
|
|
</StyledValue>
|
|
</StyledRow>
|
|
<ProgressBar
|
|
value={percentage}
|
|
barColor={
|
|
percentage > 80
|
|
? theme.color.red
|
|
: percentage > 60
|
|
? theme.color.orange
|
|
: theme.color.blue
|
|
}
|
|
backgroundColor={theme.background.quaternary}
|
|
withBorderRadius
|
|
/>
|
|
</StyledSection>
|
|
|
|
{isDefined(lastMessage) && (
|
|
<>
|
|
<StyledDivider />
|
|
<StyledSection>
|
|
<StyledSectionTitle>{t`Last message`}</StyledSectionTitle>
|
|
<StyledRow>
|
|
<StyledLabel>{t`Input tokens`}</StyledLabel>
|
|
<StyledValue>
|
|
{formatTokenCount(lastMessage.inputTokens)}
|
|
{getCachedLabel(lastMessage)}
|
|
</StyledValue>
|
|
</StyledRow>
|
|
<StyledRow>
|
|
<StyledLabel>{t`Output tokens`}</StyledLabel>
|
|
<StyledValue>
|
|
{formatTokenCount(lastMessage.outputTokens)}
|
|
</StyledValue>
|
|
</StyledRow>
|
|
<StyledRow>
|
|
<StyledLabel>{t`Cost`}</StyledLabel>
|
|
<StyledValue>
|
|
{formatCredits(
|
|
lastMessage.inputCredits + lastMessage.outputCredits,
|
|
)}{' '}
|
|
{t`credits`}
|
|
</StyledValue>
|
|
</StyledRow>
|
|
</StyledSection>
|
|
</>
|
|
)}
|
|
|
|
<StyledDivider />
|
|
<StyledSection>
|
|
<StyledSectionTitle>{t`Conversation`}</StyledSectionTitle>
|
|
<StyledRow>
|
|
<StyledLabel>{t`Input tokens`}</StyledLabel>
|
|
<StyledValue>
|
|
{formatTokenCount(agentChatUsage.inputTokens)}
|
|
</StyledValue>
|
|
</StyledRow>
|
|
<StyledRow>
|
|
<StyledLabel>{t`Output tokens`}</StyledLabel>
|
|
<StyledValue>
|
|
{formatTokenCount(agentChatUsage.outputTokens)}
|
|
</StyledValue>
|
|
</StyledRow>
|
|
<StyledRow>
|
|
<StyledLabel>{t`Total cost`}</StyledLabel>
|
|
<StyledValue>
|
|
{formatCredits(totalCredits)} {t`credits`}
|
|
</StyledValue>
|
|
</StyledRow>
|
|
</StyledSection>
|
|
</StyledHoverCard>
|
|
)}
|
|
</StyledContainer>
|
|
);
|
|
};
|