Skip to content

Commit 29b14b0

Browse files
committed
Returning full string results (before and after) from the backend to the front end
1 parent 1d0a6c5 commit 29b14b0

File tree

5 files changed

+109
-103
lines changed

5 files changed

+109
-103
lines changed

backend/src/app/schemas/query_api.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@
66

77
from app.models.query_core import Chunk, FormatType, Rule
88

9+
class ResolvedEntity(BaseModel):
10+
"""Schema for resolved entity transformations."""
11+
original: Union[str, List[str]]
12+
resolved: Union[str, List[str]]
13+
source: dict[str, str]
14+
entityType: str
15+
916

1017
class QueryPromptSchema(BaseModel):
1118
"""Schema for the prompt part of the query request."""
@@ -39,7 +46,7 @@ class QueryResult(BaseModel):
3946

4047
answer: Any
4148
chunks: List[Chunk]
42-
resolved_entities: Optional[dict[str, str]] = None
49+
resolved_entities: Optional[List[ResolvedEntity]] = None
4350

4451

4552
class QueryResponseSchema(BaseModel):
@@ -51,7 +58,7 @@ class QueryResponseSchema(BaseModel):
5158
answer: Optional[Any] = None
5259
chunks: List[Chunk]
5360
type: str
54-
resolved_entities: Optional[dict[str, str]] = None
61+
resolved_entities: Optional[List[ResolvedEntity]] = None
5562

5663

5764
class QueryAnswer(BaseModel):
@@ -69,7 +76,7 @@ class QueryAnswerResponse(BaseModel):
6976

7077
answer: QueryAnswer
7178
chunks: List[Chunk]
72-
resolved_entities: Optional[dict[str, str]] = None
79+
resolved_entities: Optional[List[ResolvedEntity]] = None
7380

7481

7582
# Type for search responses (used in service layer)

backend/src/app/services/query_service.py

Lines changed: 57 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Query service."""
22

33
import logging
4+
import re
45
from typing import Any, Awaitable, Callable, Dict, List, Union
56

67
from app.models.query_core import Chunk, FormatType, QueryType, Rule
@@ -45,61 +46,52 @@ def replace_keywords(
4546
if not text or not keyword_replacements:
4647
return text, {}
4748

48-
transformations: Dict[str, str] = {}
49-
5049
# Handle list of strings
5150
if isinstance(text, list):
51+
original_text = text.copy()
5252
result = []
53-
# Track which strings were modified
53+
modified = False
54+
55+
# Create a single regex pattern for all keywords
56+
pattern = '|'.join(map(re.escape, keyword_replacements.keys()))
57+
regex = re.compile(f'\\b({pattern})\\b')
58+
5459
for item in text:
55-
if any(keyword in item.split() for keyword in keyword_replacements):
56-
# Only process strings that contain keywords
57-
transformed_item, item_transformations = replace_keywords_in_string(item, keyword_replacements)
58-
result.append(transformed_item)
59-
# Store the full before/after for the list item
60-
transformations[item] = transformed_item
61-
else:
62-
result.append(item)
63-
return result, transformations
60+
# Single pass replacement for all keywords
61+
new_item = regex.sub(lambda m: keyword_replacements[m.group()], item)
62+
result.append(new_item)
63+
if new_item != item:
64+
modified = True
65+
66+
# Only return transformation if something actually changed
67+
if modified:
68+
return result, {
69+
"original": original_text,
70+
"resolved": result
71+
}
72+
return result, {}
6473

6574
# Handle single string
6675
return replace_keywords_in_string(text, keyword_replacements)
6776

68-
6977
def replace_keywords_in_string(
7078
text: str, keyword_replacements: dict[str, str]
7179
) -> tuple[str, dict[str, str]]:
7280
"""Keywords for single string."""
7381
if not text:
7482
return text, {}
7583

76-
result = text
77-
transformations: Dict[str, str] = {}
78-
79-
for original, new_word in keyword_replacements.items():
80-
if original in text:
81-
current_pos = 0
82-
while True:
83-
start_idx = text.find(original, current_pos)
84-
if start_idx == -1: # No more occurrences
85-
break
86-
87-
end_idx = start_idx + len(original)
88-
current_pos = end_idx
89-
90-
while end_idx < len(text) and (
91-
text[end_idx].isalnum() or text[end_idx] in "()"
92-
):
93-
end_idx += 1
94-
95-
full_original = text[start_idx:end_idx]
96-
suffix = text[start_idx + len(original) : end_idx]
97-
full_new = new_word + suffix
98-
99-
result = result.replace(full_original, full_new)
100-
transformations[full_original] = full_new
101-
102-
return result, transformations
84+
# Create a single regex pattern for all keywords
85+
pattern = '|'.join(map(re.escape, keyword_replacements.keys()))
86+
regex = re.compile(f'\\b({pattern})\\b')
87+
88+
# Single pass replacement
89+
result = regex.sub(lambda m: keyword_replacements[m.group()], text)
90+
91+
# Only return transformation if something changed
92+
if result != text:
93+
return result, {"original": text, "resolved": result}
94+
return text, {}
10395

10496

10597
async def process_query(
@@ -141,31 +133,48 @@ async def process_query(
141133
else chunks
142134
)
143135

136+
# First populate the replacements dictionary
144137
replacements: Dict[str, str] = {}
145-
146138
if resolve_entity_rules and answer_value:
147-
# Combine all replacements from all resolve_entity rules
148139
for rule in resolve_entity_rules:
149140
if rule.options:
150141
rule_replacements = dict(
151142
option.split(":") for option in rule.options
152143
)
153144
replacements.update(rule_replacements)
154145

146+
# Then apply the replacements if we have any
155147
if replacements:
156148
print(f"Resolving entities in answer: {answer_value}")
157-
# Handle both string and list cases
158-
answer_value, transformations = replace_keywords(
159-
answer_value, replacements
160-
)
149+
if isinstance(answer_value, list):
150+
# Transform the list but keep track of both original and transformed
151+
transformed_list, _ = replace_keywords(answer_value, replacements)
152+
transformations = {
153+
"original": answer_value, # Keep as list
154+
"resolved": transformed_list # Keep as list
155+
}
156+
answer_value = transformed_list
157+
else:
158+
# Handle single string case
159+
transformed_value, _ = replace_keywords(answer_value, replacements)
160+
transformations = {
161+
"original": answer_value,
162+
"resolved": transformed_value
163+
}
164+
answer_value = transformed_value
165+
161166

162167
return QueryResult(
163168
answer=answer_value,
164169
chunks=result_chunks[:10],
165-
resolved_entities=transformations if transformations else None,
170+
resolved_entities=[{
171+
"original": transformations["original"],
172+
"resolved": transformations["resolved"],
173+
"source": {"type": "column", "id": "some-id"},
174+
"entityType": "some-type"
175+
}] if transformations else None
166176
)
167177

168-
169178
# Convenience functions for specific query types
170179
async def decomposition_query(
171180
query: str,

frontend/src/components/kt/kt-controls/kt-resolved-entities.tsx

Lines changed: 11 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -29,44 +29,6 @@ export function KtResolvedEntities(props: BoxProps) {
2929
return entities;
3030
}, [table.globalRules, table.columns]);
3131

32-
// Helper to format display value based on type
33-
const getDisplayValue = (value: string, entity: ResolvedEntity, isOriginal: boolean) => {
34-
try {
35-
const parsed = JSON.parse(value);
36-
if (Array.isArray(parsed)) {
37-
if (isOriginal) {
38-
39-
return (
40-
<Code block style={{ whiteSpace: 'pre-wrap' }}>
41-
{parsed
42-
.map(item => {
43-
// Check if the item contains the resolved value
44-
const shouldReplace = item.includes(entity.resolved);
45-
// If it contains the resolved value, replace that part with the original
46-
return shouldReplace ? item.replace(entity.resolved, entity.original) : item;
47-
})
48-
.join('\n')}
49-
</Code>
50-
);
51-
} else {
52-
// For "To": Show fullAnswer as is
53-
return (
54-
<Code block style={{ whiteSpace: 'pre-wrap' }}>
55-
{parsed.join('\n')}
56-
</Code>
57-
);
58-
}
59-
}
60-
} catch {
61-
// If not an array, show the original value for "From" or the value as is for "To"
62-
return (
63-
<Code block style={{ whiteSpace: 'pre-wrap' }}>
64-
{isOriginal ? entity.original : value}
65-
</Code>
66-
);
67-
}
68-
};
69-
7032
const handleUndoTransformation = (entity: ResolvedEntity) => {
7133
const rows = table.rows.map(row => ({
7234
...row,
@@ -137,10 +99,18 @@ export function KtResolvedEntities(props: BoxProps) {
13799
<Group justify="space-between" align="flex-start" wrap="nowrap">
138100
<Stack gap="xs" style={{ flex: 1 }}>
139101
<Text size="sm" fw={500}>From:</Text>
140-
{getDisplayValue(entity.fullAnswer, entity, true)}
102+
<Code block style={{ whiteSpace: 'pre-wrap' }}>
103+
{Array.isArray(entity.original)
104+
? entity.original.join('\n') // One item per line
105+
: entity.original}
106+
</Code>
141107
<Text size="sm" fw={500}>To:</Text>
142-
{getDisplayValue(entity.fullAnswer, entity, false)}
143-
</Stack>
108+
<Code block style={{ whiteSpace: 'pre-wrap' }}>
109+
{Array.isArray(entity.resolved)
110+
? entity.resolved.join('\n') // One item per line
111+
: entity.resolved}
112+
</Code>
113+
</Stack>
144114
<Tooltip label="Undo transformation">
145115
<ActionIcon
146116
variant="subtle"

frontend/src/config/api.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,19 @@ export const answerSchema = z.union([
6161
z.array(z.string())
6262
]);
6363

64+
export const resolvedEntitySchema = z.object({
65+
original: z.union([z.string(), z.array(z.string())]),
66+
resolved: z.union([z.string(), z.array(z.string())]),
67+
source: z.object({
68+
type: z.string(),
69+
id: z.string()
70+
}),
71+
entityType: z.string()
72+
});
73+
6474
// Update the resolved entities schema to match backend format
6575
export const resolvedEntitiesSchema = z.union([
66-
z.record(z.string(), z.string()),
76+
z.array(resolvedEntitySchema),
6777
z.null(),
6878
z.undefined()
6979
]);
@@ -118,14 +128,15 @@ export async function runQuery(
118128
const parsed = queryResponseSchema.parse(response);
119129
console.log('Parsed Response:', parsed);
120130

121-
// Update resolved entities transformation to handle null/undefined
122-
const resolvedEntities = parsed.resolved_entities && typeof parsed.resolved_entities === 'object'
123-
? Object.entries(parsed.resolved_entities).map(([original, resolved]) => ({
124-
original,
125-
resolved,
126-
fullAnswer: parsed.answer.answer as string
127-
}))
128-
: null; // Change to null instead of undefined to match expected type
131+
// Update resolved entities transformation to handle the new format
132+
const resolvedEntities = parsed.resolved_entities?.map(entity => ({
133+
original: entity.original,
134+
resolved: entity.resolved,
135+
source: entity.source,
136+
entityType: entity.entityType,
137+
fullAnswer: parsed.answer.answer as string
138+
})) ?? null;
139+
129140
console.log('Transformed Resolved Entities:', resolvedEntities);
130141

131142
return {

frontend/src/config/store/store.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -400,11 +400,20 @@ export const useStore = create<Store>()(
400400
const currentTable = getTable(activeTableId);
401401

402402
// Helper to check if an entity matches any global rule patterns
403-
const isGlobalEntity = (entity: { original: string; resolved: string }) => {
403+
const isGlobalEntity = (entity: {
404+
original: string | string[];
405+
resolved: string | string[];
406+
source?: { type: string; id: string };
407+
entityType?: string
408+
}) => {
409+
const originalText = Array.isArray(entity.original)
410+
? entity.original.join(' ')
411+
: entity.original;
412+
404413
return globalRules.some(rule =>
405414
rule.type === 'resolve_entity' &&
406415
rule.options?.some(pattern =>
407-
entity.original.toLowerCase().includes(pattern.toLowerCase())
416+
originalText.toLowerCase().includes(pattern.toLowerCase())
408417
)
409418
);
410419
};

0 commit comments

Comments
 (0)