From 26b453e2dee78f3a9552f08e4b59f1df76bcfaea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Frnka?= Date: Fri, 12 Jun 2026 16:38:21 +0200 Subject: [PATCH 1/4] Removed unused data from config template --- service/config.template.yaml | 10 --- .../ai/assignment/assignment_component.py | 27 +++--- .../ai/assignment/llm.py | 5 +- .../ai/assignment/sections/llm.py | 5 +- .../ai/common/config.py | 42 +--------- .../ai/common/llm_client.py | 19 +++-- .../ai/generation/dmp_generator_component.py | 18 ++-- .../ai/generation/llm.py | 63 +++----------- .../ai/knowledgemodel/dsw_client.py | 30 +++---- .../ai/polishing/dmp_polisher_component.py | 18 ++-- .../ai/polishing/llm.py | 51 ++++++++++++ .../{ => ai}/run_pipeline.py | 44 ++++------ .../ai_document_plugin_service/api/routes.py | 4 +- .../ai_document_plugin_service/api/types.py | 6 +- .../service/pipeline_service.py | 83 ++++++------------- .../tests/generation/test_dmp_generator.py | 25 +++--- 16 files changed, 191 insertions(+), 259 deletions(-) create mode 100644 service/src/ai_document_plugin_service/ai/polishing/llm.py rename service/src/ai_document_plugin_service/{ => ai}/run_pipeline.py (89%) diff --git a/service/config.template.yaml b/service/config.template.yaml index 9cb8ab8..3f4e43b 100644 --- a/service/config.template.yaml +++ b/service/config.template.yaml @@ -1,13 +1,3 @@ -llm_response_generation: - type: "openai-api" - model: "gpt-oss-120b" - api_url: https://llm.ai.e-infra.cz/v1/ - api_key: "xxx" - workers: 4 - -dsw: - api_url: "" - auth: allowed_project_urls: - "https://your-dsw-instance.example.com" diff --git a/service/src/ai_document_plugin_service/ai/assignment/assignment_component.py b/service/src/ai_document_plugin_service/ai/assignment/assignment_component.py index e08f659..fbbc7d7 100644 --- a/service/src/ai_document_plugin_service/ai/assignment/assignment_component.py +++ b/service/src/ai_document_plugin_service/ai/assignment/assignment_component.py @@ -24,6 +24,7 @@ ) from ai_document_plugin_service.ai.assignment.types import SectionAssignment from ai_document_plugin_service.ai.common.config import Config +from ai_document_plugin_service.ai.common.llm_client import LLMClient from ai_document_plugin_service.ai.common.progress import progress_percent from ai_document_plugin_service.ai.common.types import AssignmentStats from ai_document_plugin_service.ai.knowledgemodel.types import QuestionData @@ -38,6 +39,17 @@ class AssignmentComponentResult(TypedDict): @component class AssignmentComponent: + """ + This component is responsible for assigning questions from the questionnaire to the sections from the dmp template. + For example, it assigns question 'When will the project start?' to sections Introduction and Project Timeline + """ + + def __init__(self, llm_client: LLMClient, config: Config) -> None: + self.llm_client = llm_client + self.config = config + self.section_id_generator = OpenAISectionIdGenerator(llm_client, config) + self.section_matcher = OpenAILayerMatcher(self.llm_client, self.config) + @staticmethod def _add_chunk_mapping_to_result( *, @@ -53,16 +65,13 @@ def _add_chunk_mapping_to_result( continue result_mapping[question_path] = [section_formatter.record_id_for_sid(sid) for sid in section_ids] - @staticmethod def _match_single_chunk( - *, - config: Config, + self, sections_xml: str, question_chunk: str, stats: AssignmentStats, ) -> dict[str, list[str]]: - matcher = OpenAILayerMatcher(config) - return matcher.match_questions_to_sections( + return self.section_matcher.match_questions_to_sections( sections_xml, question_chunk, stats, @@ -73,7 +82,6 @@ def run( self, data: list[QuestionData], template_data: dict[str, Any], - config: Config, km: dict[str, Any], on_progress: Callable[[str], None] | None = None, ) -> AssignmentComponentResult: @@ -85,7 +93,7 @@ def run( stats = AssignmentStats() section_formatter = SectionFormatter(sections) - section_formatter.create_mappings(OpenAISectionIdGenerator(config), stats) + section_formatter.create_mappings(self.section_id_generator, stats) sections_xml = section_formatter.get_sections_as_xml() result_mapping = {} @@ -94,7 +102,6 @@ def run( def match_chunk(question_chunk: str) -> dict[str, list[str]]: result = self._match_single_chunk( - config=config, sections_xml=sections_xml, question_chunk=question_chunk, stats=stats, @@ -109,8 +116,8 @@ def match_chunk(question_chunk: str) -> dict[str, list[str]]: for question_to_section_ids in thread_map( match_chunk, question_chunks, - max_workers=config.parallel_workers, - desc=f'Assigning questions to sections ({config.parallel_workers} workers)', + max_workers=self.llm_client.get_max_workers(), + desc=f'Assigning questions to sections ({self.llm_client.get_max_workers()} workers)', ): self._add_chunk_mapping_to_result( result_mapping=result_mapping, diff --git a/service/src/ai_document_plugin_service/ai/assignment/llm.py b/service/src/ai_document_plugin_service/ai/assignment/llm.py index 6d1b379..5728846 100644 --- a/service/src/ai_document_plugin_service/ai/assignment/llm.py +++ b/service/src/ai_document_plugin_service/ai/assignment/llm.py @@ -45,9 +45,9 @@ def match_questions_to_sections( class OpenAILayerMatcher(LayerMatcher): - def __init__(self, config: Config) -> None: + def __init__(self, llm_client: LLMClient, config: Config) -> None: + self.client = llm_client self.config = config - self.client = LLMClient(config) def match_questions_to_sections( self, @@ -74,7 +74,6 @@ def match_questions_to_sections( def call_and_parse() -> dict[str, list[str]]: response = self.client.completion( - model=self.config.model, messages=messages, temperature=self.config.assignment.temperature, max_tokens=self.config.assignment.max_tokens, diff --git a/service/src/ai_document_plugin_service/ai/assignment/sections/llm.py b/service/src/ai_document_plugin_service/ai/assignment/sections/llm.py index 20e5d24..e4d57aa 100644 --- a/service/src/ai_document_plugin_service/ai/assignment/sections/llm.py +++ b/service/src/ai_document_plugin_service/ai/assignment/sections/llm.py @@ -29,9 +29,9 @@ def generate_leaf_section_ids( class OpenAISectionIdGenerator(SectionIdGenerator): - def __init__(self, config: Config) -> None: + def __init__(self, llm_client: LLMClient, config: Config) -> None: self.config = config - self.client = LLMClient(config) + self.client = llm_client @typing.override def generate_leaf_section_ids( @@ -59,7 +59,6 @@ def generate_leaf_section_ids( ) response = call_with_retry( lambda um=user_msg: self.client.completion( - model=self.config.model, messages=[ {'role': 'system', 'content': system_msg}, {'role': 'user', 'content': um}, diff --git a/service/src/ai_document_plugin_service/ai/common/config.py b/service/src/ai_document_plugin_service/ai/common/config.py index a7559af..9402f95 100644 --- a/service/src/ai_document_plugin_service/ai/common/config.py +++ b/service/src/ai_document_plugin_service/ai/common/config.py @@ -1,6 +1,6 @@ import os import pathlib -from dataclasses import dataclass, replace +from dataclasses import dataclass from typing import Any import yaml @@ -41,11 +41,7 @@ class DatabaseConfig: @dataclass(frozen=True) class Config: - api_key: str - api_url: str - dsw_api_url: str allowed_project_urls: tuple[str, ...] - model: str log_level: str database: DatabaseConfig files: FilePaths @@ -53,11 +49,10 @@ class Config: section_id: SystemAndUserPrompt dmp_generation: SystemPrompt dmp_polishing: SystemAndUserPrompt - parallel_workers: int @dataclass(frozen=True) -class LLMConfigOverride: +class LLMConfig: model: str | None = None api_key: str | None = None api_url: str | None = None @@ -134,19 +129,6 @@ def _get_allowed_project_urls(config: dict[str, Any]) -> tuple[str, ...]: return tuple(normalized) -def _get_parallel_workers(config: dict[str, Any]) -> int: - workers = config.get('llm_response_generation', {}).get('workers', 1) - try: - workers_int = int(workers) - except (TypeError, ValueError) as exc: - msg = "Invalid config value: 'parallelism.workers' must be an integer >= 1" - raise ValueError(msg) from exc - if workers_int < 1: - msg = "Invalid config value: 'parallelism.workers' must be >= 1" - raise ValueError(msg) - return workers_int - - def _resolve_existing_path(path: str, *, base_dir: pathlib.Path | None = None) -> str: normalized_path = _normalize_path(path) path_obj = pathlib.Path(normalized_path) @@ -199,12 +181,6 @@ def load_config(config_path: str | None = None) -> Config: raise TypeError(msg) return Config( - api_key=_expand_env_vars( - _get(config, 'llm_response_generation', 'api_key'), - ), - api_url=_get(config, 'llm_response_generation', 'api_url'), - model=_get(config, 'llm_response_generation', 'model'), - dsw_api_url=_get(config, 'dsw', 'api_url'), allowed_project_urls=_get_allowed_project_urls(config), log_level=_get_log_level(config), database=DatabaseConfig( @@ -241,18 +217,4 @@ def load_config(config_path: str | None = None) -> Config: system_message=_get(prompts, 'dmp_polishing', 'system_message'), user_message=_get(prompts, 'dmp_polishing', 'user_message'), ), - parallel_workers=_get_parallel_workers(config), - ) - - -def apply_llm_override(config: Config, override: LLMConfigOverride | None = None) -> Config: - if override is None: - return config - - return replace( - config, - model=override.model or config.model, - api_key=override.api_key or config.api_key, - api_url=override.api_url or config.api_url, - parallel_workers=override.parallel_workers or config.parallel_workers, ) diff --git a/service/src/ai_document_plugin_service/ai/common/llm_client.py b/service/src/ai_document_plugin_service/ai/common/llm_client.py index e719a34..335d01f 100644 --- a/service/src/ai_document_plugin_service/ai/common/llm_client.py +++ b/service/src/ai_document_plugin_service/ai/common/llm_client.py @@ -7,7 +7,6 @@ from openai import APIConnectionError, APITimeoutError, OpenAI, RateLimitError from openai.types.chat import ChatCompletion -from ai_document_plugin_service.ai.common.config import Config from ai_document_plugin_service.ai.common.dynamic_semaphore import DynamicSemaphore if TYPE_CHECKING: @@ -76,27 +75,33 @@ def add_usage(stats: 'AssignmentStats | None', response: object) -> None: class LLMClient: - def __init__(self, config: Config) -> None: - self.client = OpenAI(api_key=config.api_key, base_url=config.api_url, max_retries=0) - self.max_workers = config.parallel_workers or 1 + def __init__(self, model: str, api_key: str, api_url: str, parallel_workers: int | None) -> None: + self.client = OpenAI(api_key=api_key, base_url=api_url, max_retries=0) + self.model = model + self.max_workers = parallel_workers or 1 logger.debug( 'Initializing LLM client, setting semaphore limit to %s', self.max_workers, ) semaphore.set_limit(self.max_workers) + def get_max_workers(self): + return self.max_workers + + def get_model_name(self): + return self.model + def completion( self, *args: Any, # noqa: ANN401 **kwargs: Any, # noqa: ANN401 ) -> ChatCompletion: req_id = uuid.uuid4().hex[:8] - model = kwargs.get('model', args[0] if args else '?') wait_start = time.perf_counter() logger.debug( '[llm] req=%s model=%s queueing (semaphore active/limit unknown until acquire)', req_id, - model, + self.model, ) with semaphore: wait_s = time.perf_counter() - wait_start @@ -107,7 +112,7 @@ def completion( semaphore.limit, ) call_start = time.perf_counter() - result = self.client.chat.completions.create(*args, **kwargs) + result = self.client.chat.completions.create(model=self.model, *args, **kwargs) logger.debug( '[llm] req=%s completed in %.3fs (releasing semaphore)', req_id, diff --git a/service/src/ai_document_plugin_service/ai/generation/dmp_generator_component.py b/service/src/ai_document_plugin_service/ai/generation/dmp_generator_component.py index 96cb329..f3ff6d1 100644 --- a/service/src/ai_document_plugin_service/ai/generation/dmp_generator_component.py +++ b/service/src/ai_document_plugin_service/ai/generation/dmp_generator_component.py @@ -16,7 +16,6 @@ from ai_document_plugin_service.ai.common.types import AssignmentStats from ai_document_plugin_service.ai.generation.llm import ( GenerationLLM, - OpenAIGenerationLLM, ) from ai_document_plugin_service.ai.generation.parse_answers import parse_answer @@ -42,16 +41,14 @@ class _ScheduledSection: @component class DmpGeneratorComponent: - def __init__(self, llm: GenerationLLM | None = None) -> None: - self.llm = llm + def __init__(self, dmp_generator_llm: GenerationLLM) -> None: + self.dmp_generator_llm = dmp_generator_llm @component.output_types(markdown=str, debug_markdown=str, stats=AssignmentStats) def run( self, replies: dict, km: dict, - config: Config, - llm: GenerationLLM | None = None, new_assignments: list[SerializedSectionAssignment] | None = None, db_assignments: list[SerializedSectionAssignment] | None = None, on_progress: Callable[[str], None] | None = None, @@ -66,18 +63,15 @@ def run( replies = self._filter_reachable_replies(replies, km) stats = AssignmentStats() - - active_llm = llm or self.llm or OpenAIGenerationLLM(config) - - worker_count = max(1, config.parallel_workers) - with ThreadPoolExecutor(max_workers=worker_count) as executor: + max_workers = self.dmp_generator_llm.get_max_workers() + with ThreadPoolExecutor(max_workers=max_workers) as executor: scheduled_sections = [ self._schedule_section( node=node, depth=0, replies=replies, km=km, - llm=active_llm, + llm=self.dmp_generator_llm, stats=stats, executor=executor, ) @@ -90,7 +84,7 @@ def run( for i, future in tqdm( enumerate(as_completed(leaf_futures), start=1), total=total_sections, - desc=f'Generating sections ({worker_count} workers)', + desc=f'Generating sections ({max_workers} workers)', ): future.result() if on_progress is not None: diff --git a/service/src/ai_document_plugin_service/ai/generation/llm.py b/service/src/ai_document_plugin_service/ai/generation/llm.py index 558d271..b46789f 100644 --- a/service/src/ai_document_plugin_service/ai/generation/llm.py +++ b/service/src/ai_document_plugin_service/ai/generation/llm.py @@ -20,6 +20,11 @@ class GenerationLLM(ABC): + + @abstractmethod + def get_max_workers(self): + pass + @abstractmethod def section_from_qa( self, @@ -29,20 +34,15 @@ def section_from_qa( ) -> str: pass - @abstractmethod - def polish_dmp( - self, - markdown: str, - structure_str: str = '', - stats: AssignmentStats | None = None, - ) -> str: - pass -class OpenAIGenerationLLM(GenerationLLM): - def __init__(self, config: Config) -> None: +class SectionGenerationLLM(GenerationLLM): + def __init__(self, llm_client: LLMClient, config: Config, ) -> None: self.config = config - self.client = LLMClient(config) + self.client = llm_client + + def get_max_workers(self): + return self.client.get_max_workers() def section_from_qa( self, @@ -66,49 +66,10 @@ def section_from_qa( ] response = call_with_retry( lambda: self.client.completion( - model=self.config.model, messages=messages, temperature=self.config.dmp_generation.temperature, max_tokens=self.config.dmp_generation.max_tokens, ), ) add_usage(stats, response) - return (response.choices[0].message.content or '').strip() - - def polish_dmp( - self, - markdown: str, - structure_str: str = '', - stats: AssignmentStats | None = None, - ) -> str: - system_prompt = self.config.dmp_polishing.system_message.replace( - '{sections}', - structure_str, - ) - user_content = self.config.dmp_polishing.user_message.replace( - '{markdown}', - markdown, - ) - system_message: ChatCompletionSystemMessageParam = { - 'role': 'system', - 'content': system_prompt, - } - user_message: ChatCompletionUserMessageParam = { - 'role': 'user', - 'content': user_content, - } - messages: list[ChatCompletionMessageParam] = [ - system_message, - user_message, - ] - - response = call_with_retry( - lambda: self.client.completion( - model=self.config.model, - messages=messages, - temperature=self.config.dmp_polishing.temperature, - max_tokens=self.config.dmp_polishing.max_tokens, - ), - ) - add_usage(stats, response) - return (response.choices[0].message.content or '').strip() + return (response.choices[0].message.content or '').strip() \ No newline at end of file diff --git a/service/src/ai_document_plugin_service/ai/knowledgemodel/dsw_client.py b/service/src/ai_document_plugin_service/ai/knowledgemodel/dsw_client.py index accad89..86f4220 100644 --- a/service/src/ai_document_plugin_service/ai/knowledgemodel/dsw_client.py +++ b/service/src/ai_document_plugin_service/ai/knowledgemodel/dsw_client.py @@ -1,21 +1,21 @@ import httpx -from ai_document_plugin_service.ai.common.config import Config +class DSWClient: + def __init__(self, token: str, api_url: str): + self.token = token + self.api_url = api_url.rstrip('/') -def get_questionnaire_detail( - questionnaire_uuid: str, - config: Config, - token: str | None = None, - api_url: str | None = None, -) -> dict: - base_url = (api_url or config.dsw_api_url).rstrip('/') - url = f'{base_url}/projects/{questionnaire_uuid}/questionnaire' + def get_questionnaire_detail( + self, + questionnaire_uuid: str + ) -> dict: + url = f'{self.api_url}/projects/{questionnaire_uuid}/questionnaire' - headers: dict[str, str] = {} - if token: - headers['Authorization'] = f'Bearer {token}' + headers: dict[str, str] = {} + if self.token: + headers['Authorization'] = f'Bearer {self.token}' - response = httpx.get(url, headers=headers) - response.raise_for_status() - return response.json() + response = httpx.get(url, headers=headers) + response.raise_for_status() + return response.json() diff --git a/service/src/ai_document_plugin_service/ai/polishing/dmp_polisher_component.py b/service/src/ai_document_plugin_service/ai/polishing/dmp_polisher_component.py index 008a1a9..769d362 100644 --- a/service/src/ai_document_plugin_service/ai/polishing/dmp_polisher_component.py +++ b/service/src/ai_document_plugin_service/ai/polishing/dmp_polisher_component.py @@ -5,7 +5,6 @@ """ import logging -import typing from collections.abc import Callable from typing import TypedDict @@ -15,7 +14,7 @@ Config, ) from ai_document_plugin_service.ai.common.types import AssignmentStats -from ai_document_plugin_service.ai.generation.llm import OpenAIGenerationLLM +from ai_document_plugin_service.ai.polishing.llm import SectionPolishingLLM logger = logging.getLogger(__name__) @@ -27,21 +26,23 @@ class DmpPolisherComponentResult(TypedDict): @component class DmpPolisherComponent: - @typing.override + def __init__(self, section_polishing_llm: SectionPolishingLLM): + self.section_polishing_llm = section_polishing_llm + @component.output_types(markdown=str, stats=AssignmentStats) def run( self, markdown: str, - config: Config, template_data: dict | None = None, on_progress: Callable[[str], None] | None = None, ) -> DmpPolisherComponentResult: """Polish the DMP by moving content to relevant sections and improving structure. Args: - markdown: The raw DMP markdown to polish. - config: Config with up to date llm config - template_data: Template dict with 'sections' key (section tree with 'title' and 'sections'). + :param markdown: The raw DMP markdown to polish. + :param config: Config with up to date llm config + :param template_data: Template dict with 'sections' key (section tree with 'title' and 'sections'). + :param on_progress: Callback method to report progress Returns: The polished DMP markdown. @@ -51,8 +52,7 @@ def run( if on_progress is not None: on_progress('Polishing document') structure_str = DmpPolisherComponent._build_template_structure_string(template_data) - llm = OpenAIGenerationLLM(config=config) - file = llm.polish_dmp( + file = self.section_polishing_llm.polish_dmp( markdown=markdown, structure_str=structure_str, stats=stats, diff --git a/service/src/ai_document_plugin_service/ai/polishing/llm.py b/service/src/ai_document_plugin_service/ai/polishing/llm.py new file mode 100644 index 0000000..19ff11e --- /dev/null +++ b/service/src/ai_document_plugin_service/ai/polishing/llm.py @@ -0,0 +1,51 @@ +from openai.types.chat import ChatCompletionSystemMessageParam, ChatCompletionUserMessageParam, \ + ChatCompletionMessageParam + +from ai_document_plugin_service.ai.common import AssignmentStats, call_with_retry, Config +from ai_document_plugin_service.ai.common.llm_client import add_usage, LLMClient + + +class SectionPolishingLLM: + def __init__(self, llm_client: LLMClient, config: Config) -> None: + self.config = config + self.client = llm_client + + def get_max_workers(self): + return self.client.get_max_workers() + + def polish_dmp( + self, + markdown: str, + structure_str: str = '', + stats: AssignmentStats | None = None, + ) -> str: + system_prompt = self.config.dmp_polishing.system_message.replace( + '{sections}', + structure_str, + ) + user_content = self.config.dmp_polishing.user_message.replace( + '{markdown}', + markdown, + ) + system_message: ChatCompletionSystemMessageParam = { + 'role': 'system', + 'content': system_prompt, + } + user_message: ChatCompletionUserMessageParam = { + 'role': 'user', + 'content': user_content, + } + messages: list[ChatCompletionMessageParam] = [ + system_message, + user_message, + ] + + response = call_with_retry( + lambda: self.client.completion( + messages=messages, + temperature=self.config.dmp_polishing.temperature, + max_tokens=self.config.dmp_polishing.max_tokens, + ), + ) + add_usage(stats, response) + return (response.choices[0].message.content or '').strip() diff --git a/service/src/ai_document_plugin_service/run_pipeline.py b/service/src/ai_document_plugin_service/ai/run_pipeline.py similarity index 89% rename from service/src/ai_document_plugin_service/run_pipeline.py rename to service/src/ai_document_plugin_service/ai/run_pipeline.py index 2f92114..acaddf4 100644 --- a/service/src/ai_document_plugin_service/run_pipeline.py +++ b/service/src/ai_document_plugin_service/ai/run_pipeline.py @@ -12,17 +12,13 @@ from ai_document_plugin_service.ai.assignment.assignment_component import AssignmentComponent from ai_document_plugin_service.ai.common import ( PipelineMetricsCollector, - configure_logging, get_component_markdown, - get_component_stats, -) -from ai_document_plugin_service.ai.common.config import ( - Config, - LLMConfigOverride, - apply_llm_override, + get_component_stats, Config, ) +from ai_document_plugin_service.ai.common.llm_client import LLMClient from ai_document_plugin_service.ai.generation.dmp_generator_component import DmpGeneratorComponent -from ai_document_plugin_service.ai.knowledgemodel.dsw_client import get_questionnaire_detail +from ai_document_plugin_service.ai.generation.llm import SectionGenerationLLM +from ai_document_plugin_service.ai.knowledgemodel.dsw_client import DSWClient from ai_document_plugin_service.ai.knowledgemodel.parser_component import ParserComponent from ai_document_plugin_service.ai.persistence.assignment_loader_component import AssignmentLoaderComponent from ai_document_plugin_service.ai.persistence.assignment_saver_component import ( @@ -32,13 +28,13 @@ ) from ai_document_plugin_service.ai.persistence.saver_component import SaverComponent from ai_document_plugin_service.ai.polishing.dmp_polisher_component import DmpPolisherComponent +from ai_document_plugin_service.ai.polishing.llm import SectionPolishingLLM if TYPE_CHECKING: from collections.abc import Mapping from haystack.components.routers.conditional_router import Route - from ai_document_plugin_service.ai.generation.llm import OpenAIGenerationLLM from ai_document_plugin_service.ai.persistence.database import Database # Cost per million tokens (USD) - adjust for your model @@ -53,15 +49,16 @@ def build_pipeline( database: Database, saver: DBSaver, - generation_llm: OpenAIGenerationLLM, + config: Config, + llm_client: LLMClient ) -> Pipeline: pipeline = Pipeline() loader_component = AssignmentLoaderComponent(database=database) parser_component = ParserComponent() - assignment_component = AssignmentComponent() + assignment_component = AssignmentComponent(llm_client, config) assignment_saver_component = AssignmentSaverComponent(saver=saver) - dmp_generator_component = DmpGeneratorComponent(llm=generation_llm) - dmp_polisher_component = DmpPolisherComponent() + dmp_generator_component = DmpGeneratorComponent(SectionGenerationLLM(llm_client, config)) + dmp_polisher_component = DmpPolisherComponent(SectionPolishingLLM(llm_client, config)) saver_component = SaverComponent(database=database) # ROUTES @@ -118,8 +115,6 @@ def build_pipeline( def run_pipeline( questionnaire_uuid: str, - token: str, - dsw_api_url: str | None, template_uuid: str, template_title: str, template_data: Mapping[str, object], @@ -127,20 +122,16 @@ def run_pipeline( tenant_uuid: str, pipeline: Pipeline, database: Database, - config: Config, - llm_override: LLMConfigOverride | None = None, + dsw_client: DSWClient, + model_name: str, on_progress: ProgressCallback | None = None, ) -> tuple[str, str]: t1 = time.time() - resolved_config = apply_llm_override(config, llm_override) - configure_logging(resolved_config.log_level) - model_name = resolved_config.model + # todo: this should maybe be more globally set somewhere? + # configure_logging(config.log_level) - km_data = get_questionnaire_detail( - questionnaire_uuid=questionnaire_uuid, - config=config, - token=token, - api_url=dsw_api_url, + km_data = dsw_client.get_questionnaire_detail( + questionnaire_uuid=questionnaire_uuid ) replies = km_data['replies'] @@ -161,7 +152,6 @@ def run_pipeline( 'parser_component': {'data': km_data}, 'assignment_component': { 'template_data': template_data, - 'config': resolved_config, 'km': km, 'on_progress': on_progress, }, @@ -176,11 +166,9 @@ def run_pipeline( 'dmp_generator_component': { 'replies': replies, 'km': km, - 'config': resolved_config, 'on_progress': on_progress, }, 'dmp_polisher_component': { - 'config': resolved_config, 'template_data': template_data, 'on_progress': on_progress, }, diff --git a/service/src/ai_document_plugin_service/api/routes.py b/service/src/ai_document_plugin_service/api/routes.py index 5fa57df..6306f0e 100644 --- a/service/src/ai_document_plugin_service/api/routes.py +++ b/service/src/ai_document_plugin_service/api/routes.py @@ -3,7 +3,7 @@ import fastapi -from ai_document_plugin_service.ai.common.config import Config, LLMConfigOverride +from ai_document_plugin_service.ai.common.config import Config, LLMConfig from ai_document_plugin_service.ai.persistence.database import PostgresDB from ai_document_plugin_service.api.auth import AuthenticatedUser, verify_authenticated from ai_document_plugin_service.api.jwt import extract_identity_from_token @@ -133,7 +133,7 @@ def start_pipeline( tenant_uuid, auth.token, auth.api_url, - LLMConfigOverride( + LLMConfig( model=payload.llm_model, api_key=payload.llm_api_key, api_url=payload.llm_api_url, diff --git a/service/src/ai_document_plugin_service/api/types.py b/service/src/ai_document_plugin_service/api/types.py index 06c7262..77b1024 100644 --- a/service/src/ai_document_plugin_service/api/types.py +++ b/service/src/ai_document_plugin_service/api/types.py @@ -46,9 +46,9 @@ class TemplateCreateRequest(ApiModel): class PipelineRunRequest(ApiModel): questionnaire_uuid: str = Field(alias='questionnaireUuid') template_uuid: str = Field(alias='templateUuid') - llm_model: str | None = Field(default=None, alias='llmModel') - llm_api_key: str | None = Field(default=None, alias='llmApiKey') - llm_api_url: str | None = Field(default=None, alias='llmApiUrl') + llm_model: str = Field(alias='llmModel') + llm_api_key: str = Field(alias='llmApiKey') + llm_api_url: str = Field(alias='llmApiUrl') llm_max_workers: int | None = Field(default=None, alias='llmMaxWorkers', ge=1) diff --git a/service/src/ai_document_plugin_service/service/pipeline_service.py b/service/src/ai_document_plugin_service/service/pipeline_service.py index 7182dde..082a960 100644 --- a/service/src/ai_document_plugin_service/service/pipeline_service.py +++ b/service/src/ai_document_plugin_service/service/pipeline_service.py @@ -2,17 +2,17 @@ import threading from datetime import UTC, datetime -from haystack.core.errors import PipelineRuntimeError from openai import AuthenticationError from ai_document_plugin_service.ai.common.config import ( Config, - LLMConfigOverride, - apply_llm_override, + LLMConfig, ) -from ai_document_plugin_service.ai.generation.llm import OpenAIGenerationLLM +from ai_document_plugin_service.ai.common.llm_client import LLMClient +from ai_document_plugin_service.ai.knowledgemodel.dsw_client import DSWClient from ai_document_plugin_service.ai.persistence.assignment_saver_component import DBSaver from ai_document_plugin_service.ai.persistence.database import PostgresDB +from ai_document_plugin_service.ai.run_pipeline import build_pipeline, run_pipeline from ai_document_plugin_service.api.types import ( ErrorType, PipelineErrorResponse, @@ -20,7 +20,6 @@ PipelineStatusResponse, _model_from_fields, ) -from ai_document_plugin_service.run_pipeline import build_pipeline, run_pipeline logger = logging.getLogger(__name__) @@ -32,6 +31,21 @@ TEMPLATE_NOT_FOUND_MESSAGE = 'Template not found.' +def _pipeline_error_from_exception(error: Exception) -> PipelineErrorResponse: + if isinstance(error, AuthenticationError) or isinstance( + error.__cause__, AuthenticationError + ): + return PipelineErrorResponse( + type=ErrorType.AUTHENTICATION_FAILED, + message=AUTHORIZATION_ERROR_MESSAGE, + ) + + return PipelineErrorResponse( + type=ErrorType.SERVER_ERROR, + message=SERVER_ERROR_MESSAGE, + ) + + def set_pipeline_status(run_id: str, status: PipelineStatusResponse) -> None: with _pipeline_runs_lock: _pipeline_runs[run_id] = status @@ -113,14 +127,13 @@ def run_pipeline_job( user_uuid: str, tenant_uuid: str, token: str, - api_url: str | None, - llm_override: LLMConfigOverride | None, + dsw_api_url: str, + llm_config: LLMConfig, config: Config, ) -> None: - resolved_config = apply_llm_override(config, llm_override) database = PostgresDB(config.database) saver = DBSaver(database) - generation_llm = OpenAIGenerationLLM(config=resolved_config) + llm_client = LLMClient(llm_config.model, llm_config.api_key, llm_config.api_url, llm_config.parallel_workers) template = database.get_template(template_uuid) if template is None: set_pipeline_status( @@ -153,21 +166,19 @@ def on_progress(message: str) -> None: ) try: - pipeline = build_pipeline(database=database, saver=saver, generation_llm=generation_llm) + pipeline = build_pipeline(database=database, saver=saver, config=config, llm_client=llm_client) knowledge_model_uuid, result = run_pipeline( questionnaire_uuid=questionnaire_uuid, - token=token, - dsw_api_url=api_url, template_uuid=template_uuid, template_title=template['title'], template_data=template['content'], user_uuid=user_uuid, tenant_uuid=tenant_uuid, pipeline=pipeline, - llm_override=llm_override, database=database, on_progress=on_progress, - config=config, + model_name=llm_client.get_model_name(), + dsw_client=DSWClient(token, dsw_api_url) ) set_pipeline_status( @@ -185,44 +196,7 @@ def on_progress(message: str) -> None: result_markdown=result, ), ) - except PipelineRuntimeError as error: - if isinstance(error.__cause__, AuthenticationError): - set_pipeline_status( - run_id, - build_pipeline_status( - run_id=run_id, - status=PipelineStatus.FAILED, - questionnaire_uuid=questionnaire_uuid, - template_uuid=template_uuid, - template_title=template_title, - user_uuid=user_uuid, - tenant_uuid=tenant_uuid, - error=PipelineErrorResponse( - type=ErrorType.AUTHENTICATION_FAILED, - message=AUTHORIZATION_ERROR_MESSAGE, - ), - ), - ) - else: - set_pipeline_status( - run_id, - build_pipeline_status( - run_id=run_id, - status=PipelineStatus.FAILED, - questionnaire_uuid=questionnaire_uuid, - template_uuid=template_uuid, - template_title=template_title, - user_uuid=user_uuid, - tenant_uuid=tenant_uuid, - error=PipelineErrorResponse( - type=ErrorType.SERVER_ERROR, - message=SERVER_ERROR_MESSAGE, - ), - ), - ) - - logger.exception('Pipeline run failed') - except AuthenticationError: + except Exception as error: set_pipeline_status( run_id, build_pipeline_status( @@ -233,10 +207,7 @@ def on_progress(message: str) -> None: template_title=template_title, user_uuid=user_uuid, tenant_uuid=tenant_uuid, - error=PipelineErrorResponse( - type=ErrorType.SERVER_ERROR, - message=SERVER_ERROR_MESSAGE, - ), + error=_pipeline_error_from_exception(error), ), ) logger.exception('Pipeline run failed') diff --git a/service/tests/generation/test_dmp_generator.py b/service/tests/generation/test_dmp_generator.py index 0928bcd..3e988db 100644 --- a/service/tests/generation/test_dmp_generator.py +++ b/service/tests/generation/test_dmp_generator.py @@ -17,8 +17,11 @@ from ai_document_plugin_service.ai.knowledgemodel.parser_component import ParserComponent -def _component() -> DmpGeneratorComponent: - return DmpGeneratorComponent() +def _component(gen_llm: GenerationLLM | None = None) -> DmpGeneratorComponent: + return DmpGeneratorComponent( + config=_test_config(), + dmp_generator_llm=gen_llm or StubGenerationLLM(), + ) def _test_config() -> Config: @@ -74,6 +77,7 @@ def _km_fixture() -> dict: }, } + def _reachable_km_fixture() -> dict: return { 'chapterUuids': ['chapter'], @@ -121,6 +125,9 @@ def _reachable_km_fixture() -> dict: class StubGenerationLLM(GenerationLLM): """Deterministic LLM stub that records calls.""" + def get_max_workers(self): + 1 + def __init__(self, section_response: str = 'Generated section body'): self.section_calls: list[str] = [] self.polish_calls: list[str] = [] @@ -478,6 +485,7 @@ def test_parse_answer_integration_reply_returns_first_raw_and_url() -> None: '"Comma-separated Values"} https://fairsharing.org/1398' ) + def test_parse_answer_integration_reply_handles_none_values_in_raw_mapping() -> None: km = { 'entities': { @@ -508,6 +516,7 @@ def test_parse_answer_integration_reply_handles_none_values_in_raw_mapping() -> '{"name": "Zenodo", "homepage": null, "url": null, "doi": null, "description": null} value' ) + def test_parser_component_item_select_reply_uses_integration_raw_url() -> None: parser = ParserComponent() parser.km = { @@ -550,7 +559,8 @@ def test_parser_component_item_select_reply_uses_integration_raw_url() -> None: def test_run_renders_parent_and_leaf_sections() -> None: - component = _component() + stub = StubGenerationLLM() + component = _component(stub) km = _km_fixture() assignments = [ SectionAssignment( @@ -576,12 +586,9 @@ def test_run_renders_parent_and_leaf_sections() -> None: 'ch.listQ.itemQ': {'value': {'type': 'AnswerReply', 'value': 'yes'}}, } - stub = StubGenerationLLM() result = component.run( replies=replies, km=km, - config=_test_config(), - llm=stub, new_assignments=_serialize_assignments(assignments), ) markdown = result['markdown'] @@ -598,18 +605,16 @@ def test_run_renders_parent_and_leaf_sections() -> None: def test_run_handles_empty_section() -> None: - component = _component() + stub = StubGenerationLLM() + component = _component(stub) km = _km_fixture() assignments = [ SectionAssignment(id='s0', title='Empty'), ] - stub = StubGenerationLLM() result = component.run( replies={}, km=km, - config=_test_config(), - llm=stub, new_assignments=_serialize_assignments(assignments), ) markdown = result['markdown'] From 95bd20ecf9d17084cbe22d8989ed127e6347fd56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Frnka?= Date: Fri, 12 Jun 2026 16:45:47 +0200 Subject: [PATCH 2/4] Fix typechecks --- .../ai/common/llm_client.py | 6 +++--- .../ai/generation/dmp_generator_component.py | 1 - .../ai/generation/llm.py | 9 ++++----- .../ai/knowledgemodel/dsw_client.py | 2 +- .../ai/polishing/dmp_polisher_component.py | 5 +---- .../ai/polishing/llm.py | 16 +++++++++++----- .../ai/run_pipeline.py | 10 ++++------ service/src/ai_document_plugin_service/app.py | 2 ++ 8 files changed, 26 insertions(+), 25 deletions(-) diff --git a/service/src/ai_document_plugin_service/ai/common/llm_client.py b/service/src/ai_document_plugin_service/ai/common/llm_client.py index 335d01f..c5553c7 100644 --- a/service/src/ai_document_plugin_service/ai/common/llm_client.py +++ b/service/src/ai_document_plugin_service/ai/common/llm_client.py @@ -85,10 +85,10 @@ def __init__(self, model: str, api_key: str, api_url: str, parallel_workers: int ) semaphore.set_limit(self.max_workers) - def get_max_workers(self): + def get_max_workers(self) -> int: return self.max_workers - def get_model_name(self): + def get_model_name(self) -> str: return self.model def completion( @@ -112,7 +112,7 @@ def completion( semaphore.limit, ) call_start = time.perf_counter() - result = self.client.chat.completions.create(model=self.model, *args, **kwargs) + result = self.client.chat.completions.create(*args, model=self.model, **kwargs) logger.debug( '[llm] req=%s completed in %.3fs (releasing semaphore)', req_id, diff --git a/service/src/ai_document_plugin_service/ai/generation/dmp_generator_component.py b/service/src/ai_document_plugin_service/ai/generation/dmp_generator_component.py index f3ff6d1..5e6d06a 100644 --- a/service/src/ai_document_plugin_service/ai/generation/dmp_generator_component.py +++ b/service/src/ai_document_plugin_service/ai/generation/dmp_generator_component.py @@ -11,7 +11,6 @@ from tqdm import tqdm from ai_document_plugin_service.ai.assignment.types import SerializedSectionAssignment -from ai_document_plugin_service.ai.common import Config from ai_document_plugin_service.ai.common.progress import progress_percent from ai_document_plugin_service.ai.common.types import AssignmentStats from ai_document_plugin_service.ai.generation.llm import ( diff --git a/service/src/ai_document_plugin_service/ai/generation/llm.py b/service/src/ai_document_plugin_service/ai/generation/llm.py index b46789f..abef746 100644 --- a/service/src/ai_document_plugin_service/ai/generation/llm.py +++ b/service/src/ai_document_plugin_service/ai/generation/llm.py @@ -22,7 +22,7 @@ class GenerationLLM(ABC): @abstractmethod - def get_max_workers(self): + def get_max_workers(self) -> int: pass @abstractmethod @@ -35,13 +35,12 @@ def section_from_qa( pass - class SectionGenerationLLM(GenerationLLM): - def __init__(self, llm_client: LLMClient, config: Config, ) -> None: + def __init__(self, llm_client: LLMClient, config: Config) -> None: self.config = config self.client = llm_client - def get_max_workers(self): + def get_max_workers(self) -> int: return self.client.get_max_workers() def section_from_qa( @@ -72,4 +71,4 @@ def section_from_qa( ), ) add_usage(stats, response) - return (response.choices[0].message.content or '').strip() \ No newline at end of file + return (response.choices[0].message.content or '').strip() diff --git a/service/src/ai_document_plugin_service/ai/knowledgemodel/dsw_client.py b/service/src/ai_document_plugin_service/ai/knowledgemodel/dsw_client.py index 86f4220..f862bf0 100644 --- a/service/src/ai_document_plugin_service/ai/knowledgemodel/dsw_client.py +++ b/service/src/ai_document_plugin_service/ai/knowledgemodel/dsw_client.py @@ -2,7 +2,7 @@ class DSWClient: - def __init__(self, token: str, api_url: str): + def __init__(self, token: str, api_url: str) -> None: self.token = token self.api_url = api_url.rstrip('/') diff --git a/service/src/ai_document_plugin_service/ai/polishing/dmp_polisher_component.py b/service/src/ai_document_plugin_service/ai/polishing/dmp_polisher_component.py index 769d362..053d074 100644 --- a/service/src/ai_document_plugin_service/ai/polishing/dmp_polisher_component.py +++ b/service/src/ai_document_plugin_service/ai/polishing/dmp_polisher_component.py @@ -10,9 +10,6 @@ from haystack import component -from ai_document_plugin_service.ai.common.config import ( - Config, -) from ai_document_plugin_service.ai.common.types import AssignmentStats from ai_document_plugin_service.ai.polishing.llm import SectionPolishingLLM @@ -26,7 +23,7 @@ class DmpPolisherComponentResult(TypedDict): @component class DmpPolisherComponent: - def __init__(self, section_polishing_llm: SectionPolishingLLM): + def __init__(self, section_polishing_llm: SectionPolishingLLM) -> None: self.section_polishing_llm = section_polishing_llm @component.output_types(markdown=str, stats=AssignmentStats) diff --git a/service/src/ai_document_plugin_service/ai/polishing/llm.py b/service/src/ai_document_plugin_service/ai/polishing/llm.py index 19ff11e..eba74af 100644 --- a/service/src/ai_document_plugin_service/ai/polishing/llm.py +++ b/service/src/ai_document_plugin_service/ai/polishing/llm.py @@ -1,8 +1,14 @@ -from openai.types.chat import ChatCompletionSystemMessageParam, ChatCompletionUserMessageParam, \ - ChatCompletionMessageParam +from typing import TYPE_CHECKING -from ai_document_plugin_service.ai.common import AssignmentStats, call_with_retry, Config -from ai_document_plugin_service.ai.common.llm_client import add_usage, LLMClient +from ai_document_plugin_service.ai.common import AssignmentStats, Config, call_with_retry +from ai_document_plugin_service.ai.common.llm_client import LLMClient, add_usage + +if TYPE_CHECKING: + from openai.types.chat import ( + ChatCompletionMessageParam, + ChatCompletionSystemMessageParam, + ChatCompletionUserMessageParam, + ) class SectionPolishingLLM: @@ -10,7 +16,7 @@ def __init__(self, llm_client: LLMClient, config: Config) -> None: self.config = config self.client = llm_client - def get_max_workers(self): + def get_max_workers(self) -> int: return self.client.get_max_workers() def polish_dmp( diff --git a/service/src/ai_document_plugin_service/ai/run_pipeline.py b/service/src/ai_document_plugin_service/ai/run_pipeline.py index acaddf4..adcb726 100644 --- a/service/src/ai_document_plugin_service/ai/run_pipeline.py +++ b/service/src/ai_document_plugin_service/ai/run_pipeline.py @@ -11,14 +11,13 @@ from ai_document_plugin_service.ai.assignment.assignment_component import AssignmentComponent from ai_document_plugin_service.ai.common import ( + Config, PipelineMetricsCollector, get_component_markdown, - get_component_stats, Config, + get_component_stats, ) -from ai_document_plugin_service.ai.common.llm_client import LLMClient from ai_document_plugin_service.ai.generation.dmp_generator_component import DmpGeneratorComponent from ai_document_plugin_service.ai.generation.llm import SectionGenerationLLM -from ai_document_plugin_service.ai.knowledgemodel.dsw_client import DSWClient from ai_document_plugin_service.ai.knowledgemodel.parser_component import ParserComponent from ai_document_plugin_service.ai.persistence.assignment_loader_component import AssignmentLoaderComponent from ai_document_plugin_service.ai.persistence.assignment_saver_component import ( @@ -35,6 +34,8 @@ from haystack.components.routers.conditional_router import Route + from ai_document_plugin_service.ai.common.llm_client import LLMClient + from ai_document_plugin_service.ai.knowledgemodel.dsw_client import DSWClient from ai_document_plugin_service.ai.persistence.database import Database # Cost per million tokens (USD) - adjust for your model @@ -127,9 +128,6 @@ def run_pipeline( on_progress: ProgressCallback | None = None, ) -> tuple[str, str]: t1 = time.time() - # todo: this should maybe be more globally set somewhere? - # configure_logging(config.log_level) - km_data = dsw_client.get_questionnaire_detail( questionnaire_uuid=questionnaire_uuid ) diff --git a/service/src/ai_document_plugin_service/app.py b/service/src/ai_document_plugin_service/app.py index 79db6a0..12976c9 100644 --- a/service/src/ai_document_plugin_service/app.py +++ b/service/src/ai_document_plugin_service/app.py @@ -1,6 +1,7 @@ import fastapi import fastapi.middleware.cors +from ai_document_plugin_service.ai.common import configure_logging from ai_document_plugin_service.ai.common.config import load_config, resolve_config_path from ai_document_plugin_service.ai.persistence.migrations import run_startup_migrations from ai_document_plugin_service.api.routes import protected_router, public_router @@ -9,6 +10,7 @@ def create_app(*, run_migrations: bool = True) -> fastapi.FastAPI: config_path = resolve_config_path() config = load_config(config_path) + configure_logging(config.log_level) if run_migrations: run_startup_migrations(config, config_path) From 3f80099c89fdf9c748bb01e287091215b5ab5bfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Frnka?= Date: Fri, 12 Jun 2026 16:49:32 +0200 Subject: [PATCH 3/4] Fix typechecks --- .../ai/assignment/sections/llm.py | 2 - .../ai/common/config.py | 6 +-- service/tests/common/test_config.py | 4 -- .../tests/generation/test_dmp_generator.py | 40 ------------------- 4 files changed, 3 insertions(+), 49 deletions(-) diff --git a/service/src/ai_document_plugin_service/ai/assignment/sections/llm.py b/service/src/ai_document_plugin_service/ai/assignment/sections/llm.py index e4d57aa..58fe939 100644 --- a/service/src/ai_document_plugin_service/ai/assignment/sections/llm.py +++ b/service/src/ai_document_plugin_service/ai/assignment/sections/llm.py @@ -1,7 +1,6 @@ import typing from abc import ABC, abstractmethod -from openai import OpenAI from tqdm import tqdm from ai_document_plugin_service.ai.assignment.types import LeafSection @@ -91,7 +90,6 @@ def _normalize_section_id(raw: str) -> str: class LoggingNoopSectionIdGenerator(SectionIdGenerator): def __init__(self, config: Config) -> None: self.config = config - self.client = OpenAI(api_key=config.api_key, base_url=config.api_url) @typing.override def generate_leaf_section_ids( # ty: ignore[invalid-method-override] diff --git a/service/src/ai_document_plugin_service/ai/common/config.py b/service/src/ai_document_plugin_service/ai/common/config.py index 9402f95..4be249f 100644 --- a/service/src/ai_document_plugin_service/ai/common/config.py +++ b/service/src/ai_document_plugin_service/ai/common/config.py @@ -53,9 +53,9 @@ class Config: @dataclass(frozen=True) class LLMConfig: - model: str | None = None - api_key: str | None = None - api_url: str | None = None + model: str + api_key: str + api_url: str parallel_workers: int | None = None diff --git a/service/tests/common/test_config.py b/service/tests/common/test_config.py index a41b10d..ea16fac 100644 --- a/service/tests/common/test_config.py +++ b/service/tests/common/test_config.py @@ -98,8 +98,6 @@ def test_load_config_uses_env_config_path_and_resolves_prompts_relative_to_it( config = load_config() - assert config.model == 'env-model' - assert config.api_key == 'secret-from-env' assert config.allowed_project_urls == ('https://dsw.example.com',) assert config.files.prompts_path == str(config_path.parent / 'nested/prompts.custom.yaml') @@ -115,8 +113,6 @@ def test_load_config_falls_back_to_default_path_when_env_is_missing( config = load_config() - assert config.model == 'default-model' - assert config.api_key == 'default-secret' assert config.files.prompts_path == str(config_path.parent / 'prompts.yaml') diff --git a/service/tests/generation/test_dmp_generator.py b/service/tests/generation/test_dmp_generator.py index 3e988db..2d6321a 100644 --- a/service/tests/generation/test_dmp_generator.py +++ b/service/tests/generation/test_dmp_generator.py @@ -1,13 +1,6 @@ from typing import Optional from ai_document_plugin_service.ai.assignment.types import SectionAssignment, SerializedSectionAssignment -from ai_document_plugin_service.ai.common.config import ( - Config, - DatabaseConfig, - FilePaths, - SystemAndUserPrompt, - SystemPrompt, -) from ai_document_plugin_service.ai.common.types import AssignmentStats from ai_document_plugin_service.ai.generation.dmp_generator_component import ( DmpGeneratorComponent, @@ -19,43 +12,10 @@ def _component(gen_llm: GenerationLLM | None = None) -> DmpGeneratorComponent: return DmpGeneratorComponent( - config=_test_config(), dmp_generator_llm=gen_llm or StubGenerationLLM(), ) -def _test_config() -> Config: - prompt = SystemAndUserPrompt( - temperature=0.0, - max_tokens=1, - system_message='', - user_message='', - ) - system_prompt = SystemPrompt(temperature=0.0, max_tokens=1, system_message='') - return Config( - api_key='', - api_url='', - dsw_api_url='', - allowed_project_urls=(), - model='test', - log_level='DEBUG', - database=DatabaseConfig( - host='', - port=0, - name='', - user='', - password='', - schema='', - ), - files=FilePaths(prompts_path=''), - assignment=prompt, - section_id=prompt, - dmp_generation=system_prompt, - dmp_polishing=prompt, - parallel_workers=1, - ) - - def _serialize_assignments(assignments: list[SectionAssignment]) -> list[SerializedSectionAssignment]: return [assignment.to_dict() for assignment in assignments] From 3d000bf54807b586e6c502f463242f7eee805fe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Frnka?= Date: Fri, 12 Jun 2026 16:50:21 +0200 Subject: [PATCH 4/4] Updated error handling on frontend --- plugin/src/client.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/plugin/src/client.ts b/plugin/src/client.ts index a925470..fba6421 100644 --- a/plugin/src/client.ts +++ b/plugin/src/client.ts @@ -135,11 +135,17 @@ export const runPipeline = async ({ }), }) - const data = await readApiResponse(response, url) + const data = await readApiResponse(response, url) if (!response.ok) { + if (response.status == 422) { + throw new Error( + 'Plugin is not configured. Set the model, API key, and API URL in the plugin settings.', + ) + } + const detail = 'detail' in data ? data.detail : undefined throw new Error( - 'detail' in data && data.detail ? data.detail : 'Pipeline execution failed.', + typeof detail === 'string' && detail ? detail : 'Pipeline execution failed.', ) }