Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,6 @@ function AddLlmProfile({

useEffect(() => {
setAdaptorProfilesDropdown();

return () => {
setEditLlmProfileId(null);
};
}, []);

// Load retrieval strategies when tool_id is available (only once)
Expand Down Expand Up @@ -163,7 +159,7 @@ function AddLlmProfile({

useEffect(() => {
if (resetForm) {
form.resetFields();
form.setFieldsValue(formDetails);
setResetForm(false);
}
}, [formDetails]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ function ManageLlmProfiles() {
}, [llmProfiles, defaultLlmProfile]);

const handleAddNewLlmProfileBtnClick = () => {
setEditLlmProfileId(null);
setIsAddLlm(true);

try {
Expand Down
1 change: 0 additions & 1 deletion frontend/src/components/settings/settings/Settings.css
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
}

.settings-menu-item:first-child {
font-weight: 600;
margin-bottom: 4px;
}

Expand Down
119 changes: 108 additions & 11 deletions workers/executor/executors/legacy_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -1494,7 +1494,24 @@
return

if output.get(PSKeys.TYPE) == PSKeys.LINE_ITEM:
raise LegacyExecutorError(message="LINE_ITEM extraction is not supported.")
self._run_line_item_extraction(
output=output,
context=context,
structured_output=structured_output,
metadata=metadata,
metrics=metrics,
run_id=run_id,
execution_id=execution_id,
execution_source=execution_source,
platform_api_key=platform_api_key,
tool_id=tool_id,
doc_name=doc_name,
prompt_name=prompt_name,
file_path=file_path,
tool_settings=tool_settings,
shim=shim,
)
return

usage_kwargs = {"run_id": run_id, "execution_id": execution_id}
try:
Expand Down Expand Up @@ -1700,6 +1717,83 @@
)
shim.stream_log(f"Completed prompt: {prompt_name}")

def _run_line_item_extraction(
self,
output: dict[str, Any],
context: ExecutionContext,
structured_output: dict[str, Any],
metadata: dict[str, Any],
metrics: dict[str, Any],
run_id: str,
execution_id: str,
execution_source: str,
platform_api_key: str,
tool_id: str,
doc_name: str,
prompt_name: str,
file_path: str,
tool_settings: dict[str, Any],
shim: Any,

Check warning on line 1736 in workers/executor/executors/legacy_executor.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Method "_run_line_item_extraction" has 15 parameters, which is greater than the 13 authorized.

See more on https://sonarcloud.io/project/issues?id=Zipstack_unstract&issues=AZ1h8fm6vQ1XtxwPLtJe&open=AZ1h8fm6vQ1XtxwPLtJe&pullRequest=1899
) -> None:
"""Delegate LINE_ITEM prompt to the line_item executor plugin."""
from executor.executors.constants import PromptServiceConstants as PSKeys

try:
line_item_executor = ExecutorRegistry.get("line_item")
except KeyError:
raise LegacyExecutorError(
message=(
"LINE_ITEM extraction requires the line_item executor "
"plugin. Install the line_item_extractor plugin."
)
)
line_item_ctx = ExecutionContext(
executor_name="line_item",
operation="line_item_extract",
run_id=run_id,
execution_source=execution_source,
organization_id=context.organization_id,
request_id=context.request_id,
executor_params={
"llm_adapter_instance_id": output.get(PSKeys.LLM, ""),
"tool_settings": tool_settings,
"output": output,
"prompt": output.get(PSKeys.PROMPTX, ""),
"file_path": file_path,
"PLATFORM_SERVICE_API_KEY": platform_api_key,
"execution_id": execution_id,
"tool_id": tool_id,
"file_name": doc_name,
"prompt_name": prompt_name,
},
)
line_item_ctx._log_component = self._log_component
line_item_ctx.log_events_id = self._log_events_id

shim.stream_log(f"Running line-item extraction for: {prompt_name}")
line_item_result = line_item_executor.execute(line_item_ctx)

if line_item_result.success:
data = line_item_result.data or {}
structured_output[prompt_name] = data.get("output", "")
line_item_metrics = data.get("metadata", {}).get("metrics", {})
metrics.setdefault(prompt_name, {}).update(
{"line_item_extraction": line_item_metrics}
)
context_list = data.get("context")
if context_list:
metadata[PSKeys.CONTEXT][prompt_name] = context_list
shim.stream_log(f"Line-item extraction completed for: {prompt_name}")
logger.info("LINE_ITEM extraction completed: prompt=%s", prompt_name)
else:
structured_output[prompt_name] = ""
logger.error(
"LINE_ITEM extraction failed for prompt=%s: %s",
prompt_name,
line_item_result.error,
)
shim.stream_log(f"Completed prompt: {prompt_name}")

@staticmethod
def _apply_type_conversion(
output: dict[str, Any],
Expand Down Expand Up @@ -1732,16 +1826,19 @@
)

elif output_type == PSKeys.EMAIL:
email_prompt = (
f"Extract the email from the following text:\n{answer}"
f"\n\nOutput just the email. "
f"The email should be directly assignable to a string "
f"variable. No explanation is required. If you cannot "
f'extract the email, output "NA".'
)
structured_output[prompt_name] = LegacyExecutor._convert_scalar_answer(
answer, llm, answer_prompt_svc, email_prompt
)
if answer.lower() == "na":
structured_output[prompt_name] = answer
else:
email_prompt = (
f"Extract the email from the following text:\n{answer}"
f"\n\nOutput just the email. "
f"The email should be directly assignable to a string "
f"variable. No explanation is required. If you cannot "
f'extract the email, output "NA".'
)
structured_output[prompt_name] = answer_prompt_svc.run_completion(
llm=llm, prompt=email_prompt
)

elif output_type == PSKeys.DATE:
date_prompt = (
Expand Down
16 changes: 8 additions & 8 deletions workers/tests/test_answer_prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -562,8 +562,8 @@ def test_invalid_strategy_skips_retrieval(
)
result = executor._handle_answer_prompt(ctx)

# Answer stays "NA" which gets sanitized to None
assert result.data[PSKeys.OUTPUT]["field_a"] is None
# Answer stays "NA" (top-level NA is preserved, not sanitized)
assert result.data[PSKeys.OUTPUT]["field_a"] == "NA"


class TestHandleAnswerPromptMultiPrompt:
Expand Down Expand Up @@ -687,21 +687,21 @@ def test_vectordb_closed(self, mock_shim_cls, mock_deps):
class TestNullSanitization:
"""Tests for _sanitize_null_values."""

def test_na_string_becomes_none(self):
"""Top-level 'NA' string None."""
def test_na_string_preserved(self):
"""Top-level 'NA' string is preserved (not sanitized to None)."""
from executor.executors.legacy_executor import LegacyExecutor

output = {"field": "NA"}
result = LegacyExecutor._sanitize_null_values(output)
assert result["field"] is None
assert result["field"] == "NA"

def test_na_case_insensitive(self):
"""'na' (lowercase) None."""
def test_na_case_insensitive_preserved(self):
"""Top-level 'na' (lowercase) is preserved (not sanitized to None)."""
from executor.executors.legacy_executor import LegacyExecutor

output = {"field": "na"}
result = LegacyExecutor._sanitize_null_values(output)
assert result["field"] is None
assert result["field"] == "na"

def test_nested_list_na(self):
"""NA in nested list items → None."""
Expand Down
Loading