Skip to content

Commit dcda806

Browse files
author
Antonio Maiolo
authored
feat(app,backend): RAG pipeline improvements and token estimation (#31)
1 parent 4ea2085 commit dcda806

13 files changed

Lines changed: 418 additions & 182 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
"think-app": patch
3+
---
4+
5+
# RAG Pipeline Improvements & Token Estimation
6+
7+
## Frontend Changes
8+
9+
### Token Usage Estimation
10+
- Replaced API-provided token counts with client-side estimation (~4 chars/token)
11+
- Context usage indicator now shows approximate values with `~` prefix
12+
- More stable UI without flickering from API response timing
13+
14+
### Model Selector Optimization
15+
- Added provider tracking to prevent unnecessary re-fetches during polling
16+
- Reduces API calls and eliminates visual flickering
17+
18+
## Backend Changes
19+
20+
### Improved Memory Filtering
21+
- Dynamic threshold-based filtering adapts to match quality
22+
- Tiered filtering: excellent matches allow more results, marginal matches are stricter
23+
- Skip RAG for very short messages (< 10 chars)
24+
25+
### Enhanced Search Pipeline
26+
- Added match type tracking (vector/keyword/hybrid) and RRF scores
27+
- Graceful fallback to vector-only search if hybrid fails
28+
- Comprehensive logging throughout the search pipeline
29+
30+
### Embedding Safety
31+
- Model-specific context windows (Ollama models have smaller limits than documented)
32+
- Intelligent text chunking with paragraph/sentence awareness
33+
- Parallel chunk processing with embedding averaging
34+
35+
### Other Improvements
36+
- Blocked `all-minilm` embedding model (context too small)
37+
- Removed conversation history limit for fuller context
38+
- Added logging for query transformations

app/src/components/ContextUsageIndicator.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,23 @@ import { cn } from "@/lib/utils";
33
import type { TokenUsage } from "@/types/chat";
44

55
interface ContextUsageIndicatorProps {
6-
contextUsage: TokenUsage | null; // Latest message (for context window %)
6+
estimatedTokens: number; // Estimated conversation tokens
77
billingUsage: TokenUsage | null; // Cumulative (for session totals)
88
contextWindow: number;
99
className?: string;
1010
}
1111

1212
export function ContextUsageIndicator({
13-
contextUsage,
13+
estimatedTokens,
1414
billingUsage,
1515
contextWindow,
1616
className,
1717
}: ContextUsageIndicatorProps) {
1818
const [showPopover, setShowPopover] = useState(false);
1919

20-
if (!contextUsage) return null;
20+
if (estimatedTokens === 0) return null;
2121

22-
const percentage = Math.min((contextUsage.total_tokens / contextWindow) * 100, 100);
22+
const percentage = Math.min((estimatedTokens / contextWindow) * 100, 100);
2323

2424
// Color based on usage level
2525
const getStrokeColor = () => {
@@ -78,9 +78,9 @@ export function ContextUsageIndicator({
7878
<div className="text-xs font-medium mb-2">Context Window</div>
7979
<div className="space-y-1.5 text-xs">
8080
<div className="flex justify-between">
81-
<span className="text-muted-foreground">Current:</span>
81+
<span className="text-muted-foreground">Conversation:</span>
8282
<span className="font-mono font-medium">
83-
{contextUsage.total_tokens.toLocaleString()} / {contextWindow.toLocaleString()}
83+
~{estimatedTokens.toLocaleString()} / {contextWindow.toLocaleString()}
8484
</span>
8585
</div>
8686
<div className="flex justify-between text-muted-foreground">

app/src/components/ModelSelector.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export function ModelSelector({ type = "chat", provider, selectedModel, onModelC
3333
const [pullingModel, setPullingModel] = useState<string | null>(null);
3434
const [pullProgress, setPullProgress] = useState(0);
3535
const dropdownRef = useRef<HTMLDivElement>(null);
36+
const lastFetchedProviderRef = useRef<string | undefined>(undefined);
3637

3738
// Use selectedModel prop if provided (controlled), otherwise use internal state
3839
const displayModel = selectedModel !== undefined ? selectedModel : currentModel;
@@ -61,14 +62,20 @@ export function ModelSelector({ type = "chat", provider, selectedModel, onModelC
6162

6263
// Initial fetch on mount or when type/provider changes
6364
useEffect(() => {
65+
// Skip if we already fetched for this provider (prevents flashing during polling)
66+
if (lastFetchedProviderRef.current === provider && models.length > 0) {
67+
return;
68+
}
69+
lastFetchedProviderRef.current = provider;
70+
6471
setCurrentModel(""); // Clear stale model before fetch
6572
fetchModels().then((data) => {
6673
// Notify parent of the initial model (for controlled mode)
6774
if (data && type === "embedding" && selectedModel === undefined) {
6875
onModelChange?.(data.current_model);
6976
}
7077
});
71-
}, [fetchModels, type, selectedModel, onModelChange]);
78+
}, [fetchModels, type, selectedModel, onModelChange, provider, models.length]);
7279

7380
// Close dropdown when clicking outside
7481
useEffect(() => {

app/src/contexts/ConversationContext.tsx

Lines changed: 21 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ interface ConversationContextType {
88
allSources: SourceMemory[];
99
isLoadingMessages: boolean;
1010
pendingMessage: string | null;
11-
contextUsage: TokenUsage | null; // Latest message only (for context window %)
11+
estimatedTokens: number; // Estimated conversation tokens (stable, grows with messages)
1212
billingUsage: TokenUsage | null; // Cumulative (for cost tracking)
1313
contextWindow: number;
1414
selectConversation: (conversation: Conversation | null) => void;
@@ -18,42 +18,34 @@ interface ConversationContextType {
1818
updateMessage: (id: string | number, updates: Partial<ChatMessage>) => void;
1919
clearMessages: () => void;
2020
setPendingMessage: (message: string | null) => void;
21-
updateUsage: (usage: TokenUsage | null, contextWindow?: number) => void;
21+
updateContextWindow: (contextWindow: number) => void;
2222
}
2323

2424
const ConversationContext = createContext<ConversationContextType | null>(null);
2525

26+
// Estimate tokens from text (~4 chars per token is a common approximation)
27+
const SYSTEM_PROMPT_TOKENS = 80; // Approximate tokens for the system prompt
28+
29+
function estimateTokens(text: string): number {
30+
return Math.ceil(text.length / 4);
31+
}
32+
2633
export function ConversationProvider({ children }: { children: ReactNode }) {
2734
const [currentConversationId, setCurrentConversationId] = useState<number | null>(null);
2835
const [messages, setMessages] = useState<ChatMessage[]>([]);
2936
const [isLoadingMessages, setIsLoadingMessages] = useState(false);
3037
const [pendingMessage, setPendingMessage] = useState<string | null>(null);
31-
const [contextUsage, setContextUsage] = useState<TokenUsage | null>(null); // Latest only
3238
const [billingUsage, setBillingUsage] = useState<TokenUsage | null>(null); // Cumulative
3339
const [contextWindow, setContextWindow] = useState(128000);
3440

35-
const updateUsage = useCallback((newUsage: TokenUsage | null, newContextWindow?: number) => {
36-
if (newUsage) {
37-
// Context usage = latest message only (for context window %)
38-
setContextUsage(newUsage);
39-
40-
// Billing usage = accumulate across conversation (for cost tracking)
41-
setBillingUsage((prev) =>
42-
prev
43-
? {
44-
prompt_tokens: prev.prompt_tokens + newUsage.prompt_tokens,
45-
completion_tokens: prev.completion_tokens + newUsage.completion_tokens,
46-
total_tokens: prev.total_tokens + newUsage.total_tokens,
47-
}
48-
: newUsage
49-
);
50-
} else {
51-
setContextUsage(null);
52-
setBillingUsage(null);
53-
}
54-
if (newContextWindow) {
55-
setContextWindow(newContextWindow);
56-
}
41+
// Estimate conversation tokens from messages (stable, grows with conversation)
42+
const estimatedTokens = useMemo(() => {
43+
const messageTokens = messages.reduce((acc, msg) => acc + estimateTokens(msg.content), 0);
44+
return SYSTEM_PROMPT_TOKENS + messageTokens;
45+
}, [messages]);
46+
47+
const updateContextWindow = useCallback((newContextWindow: number) => {
48+
setContextWindow(newContextWindow);
5749
}, []);
5850

5951
const loadConversation = useCallback(async (conversationId: number) => {
@@ -70,24 +62,10 @@ export function ConversationProvider({ children }: { children: ReactNode }) {
7062
}))
7163
);
7264

73-
// Get usage from messages
65+
// Billing usage = sum of all assistant message tokens (for cost tracking)
7466
const assistantMessages = data.messages.filter(
7567
(m) => m.role === "assistant" && m.total_tokens
7668
);
77-
78-
// Context usage = last assistant message only (current context window state)
79-
const lastAssistant = assistantMessages[assistantMessages.length - 1];
80-
if (lastAssistant) {
81-
setContextUsage({
82-
prompt_tokens: lastAssistant.prompt_tokens || 0,
83-
completion_tokens: lastAssistant.completion_tokens || 0,
84-
total_tokens: lastAssistant.total_tokens || 0,
85-
});
86-
} else {
87-
setContextUsage(null);
88-
}
89-
90-
// Billing usage = sum of all (total tokens consumed)
9169
const totalBilling = assistantMessages.reduce(
9270
(acc, m) => ({
9371
prompt_tokens: acc.prompt_tokens + (m.prompt_tokens || 0),
@@ -110,7 +88,7 @@ export function ConversationProvider({ children }: { children: ReactNode }) {
11088
}
11189
}, []);
11290

113-
// Aggregate all sources from messages, deduplicated by id
91+
// Aggregate all sources from conversation, deduplicated by id
11492
const allSources = useMemo(() => {
11593
const sourceMap = new Map<number, SourceMemory>();
11694
for (const msg of messages) {
@@ -133,7 +111,6 @@ export function ConversationProvider({ children }: { children: ReactNode }) {
133111
} else {
134112
setCurrentConversationId(null);
135113
setMessages([]);
136-
setContextUsage(null);
137114
setBillingUsage(null);
138115
}
139116
},
@@ -143,7 +120,6 @@ export function ConversationProvider({ children }: { children: ReactNode }) {
143120
const startNewChat = useCallback(() => {
144121
setCurrentConversationId(null);
145122
setMessages([]);
146-
setContextUsage(null);
147123
setBillingUsage(null);
148124
}, []);
149125

@@ -169,7 +145,7 @@ export function ConversationProvider({ children }: { children: ReactNode }) {
169145
allSources,
170146
isLoadingMessages,
171147
pendingMessage,
172-
contextUsage,
148+
estimatedTokens,
173149
billingUsage,
174150
contextWindow,
175151
selectConversation,
@@ -179,7 +155,7 @@ export function ConversationProvider({ children }: { children: ReactNode }) {
179155
updateMessage,
180156
clearMessages,
181157
setPendingMessage,
182-
updateUsage,
158+
updateContextWindow,
183159
}}
184160
>
185161
{children}

app/src/pages/ChatPage.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export default function ChatPage() {
2727
allSources,
2828
isLoadingMessages,
2929
pendingMessage,
30-
contextUsage,
30+
estimatedTokens,
3131
billingUsage,
3232
contextWindow,
3333
setCurrentConversationId,
@@ -36,7 +36,7 @@ export default function ChatPage() {
3636
selectConversation,
3737
startNewChat,
3838
setPendingMessage,
39-
updateUsage,
39+
updateContextWindow,
4040
} = useConversation();
4141

4242
const { conversations } = useConversations();
@@ -152,9 +152,9 @@ export default function ChatPage() {
152152
updateMessage(assistantMessageId, { content });
153153
} else if (data.type === "done") {
154154
updateMessage(assistantMessageId, { isStreaming: false });
155-
// Update usage from stream response
156-
if (data.usage) {
157-
updateUsage(data.usage, data.context_window);
155+
// Update context window from stream response
156+
if (data.context_window) {
157+
updateContextWindow(data.context_window);
158158
}
159159
} else if (data.type === "error") {
160160
updateMessage(assistantMessageId, {
@@ -180,8 +180,8 @@ export default function ChatPage() {
180180
updateMessage(assistantMessageId, { content });
181181
} else if (data.type === "done") {
182182
updateMessage(assistantMessageId, { isStreaming: false });
183-
if (data.usage) {
184-
updateUsage(data.usage, data.context_window);
183+
if (data.context_window) {
184+
updateContextWindow(data.context_window);
185185
}
186186
}
187187
} catch {
@@ -200,7 +200,7 @@ export default function ChatPage() {
200200
// Clear pending conversation ref
201201
pendingConversationRef.current = null;
202202
}
203-
}, [addMessage, updateMessage, setCurrentConversationId, updateUsage]);
203+
}, [addMessage, updateMessage, setCurrentConversationId, updateContextWindow]);
204204

205205
// Handler for manual chat input
206206
const handleChat = useCallback(() => {
@@ -267,7 +267,7 @@ export default function ChatPage() {
267267
/>
268268
</div>
269269
<ContextUsageIndicator
270-
contextUsage={contextUsage}
270+
estimatedTokens={estimatedTokens}
271271
billingUsage={billingUsage}
272272
contextWindow={contextWindow}
273273
/>

0 commit comments

Comments
 (0)