import json
import logging
import re
from collections.abc import Sequence
from typing import Protocol, cast

import json_repair

from core.llm_generator.output_parser.rule_config_generator import RuleConfigGeneratorOutputParser
from core.llm_generator.output_parser.suggested_questions_after_answer import SuggestedQuestionsAfterAnswerOutputParser
from core.llm_generator.prompts import (
    CONVERSATION_TITLE_PROMPT,
    GENERATOR_QA_PROMPT,
    JAVASCRIPT_CODE_GENERATOR_PROMPT_TEMPLATE,
    LLM_MODIFY_CODE_SYSTEM,
    LLM_MODIFY_PROMPT_SYSTEM,
    PYTHON_CODE_GENERATOR_PROMPT_TEMPLATE,
    SUGGESTED_QUESTIONS_MAX_TOKENS,
    SUGGESTED_QUESTIONS_TEMPERATURE,
    SYSTEM_STRUCTURED_OUTPUT_GENERATE,
    WORKFLOW_RULE_CONFIG_PROMPT_GENERATE_TEMPLATE,
)
from core.model_manager import ModelManager
from core.model_runtime.entities.llm_entities import LLMResult
from core.model_runtime.entities.message_entities import PromptMessage, SystemPromptMessage, UserPromptMessage
from core.model_runtime.entities.model_entities import ModelType
from core.model_runtime.errors.invoke import InvokeAuthorizationError, InvokeError
from core.ops.entities.trace_entity import OperationType
from core.ops.utils import measure_time
from core.prompt.utils.prompt_template_parser import PromptTemplateParser
from core.telemetry import TelemetryContext, TelemetryEvent, TraceTaskName
from core.telemetry import emit as telemetry_emit
from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionMetadataKey
from extensions.ext_database import db
from extensions.ext_storage import storage
from models import App, Message, WorkflowNodeExecutionModel
from models.workflow import Workflow

logger = logging.getLogger(__name__)


class WorkflowServiceInterface(Protocol):
    def get_draft_workflow(self, app_model: App, workflow_id: str | None = None) -> Workflow | None:
        pass

    def get_node_last_run(self, app_model: App, workflow: Workflow, node_id: str) -> WorkflowNodeExecutionModel | None:
        pass


class LLMGenerator:
    @classmethod
    def generate_conversation_name(
        cls, tenant_id: str, query, conversation_id: str | None = None, app_id: str | None = None
    ):
        prompt = CONVERSATION_TITLE_PROMPT

        if len(query) > 2000:
            query = query[:300] + "...[TRUNCATED]..." + query[-300:]

        query = query.replace("\n", " ")

        prompt += query + "\n"

        model_manager = ModelManager()
        model_instance = model_manager.get_default_model_instance(
            tenant_id=tenant_id,
            model_type=ModelType.LLM,
        )
        prompts = [UserPromptMessage(content=prompt)]

        with measure_time() as timer:
            response: LLMResult = model_instance.invoke_llm(
                prompt_messages=list(prompts), model_parameters={"max_tokens": 500, "temperature": 1}, stream=False
            )
        answer = cast(str, response.message.content)
        if answer is None:
            return ""
        try:
            result_dict = json.loads(answer)
        except json.JSONDecodeError:
            result_dict = json_repair.loads(answer)

        if not isinstance(result_dict, dict):
            answer = query
        else:
            output = result_dict.get("Your Output")
            if isinstance(output, str) and output.strip():
                answer = output.strip()
            else:
                answer = query

        name = answer.strip()

        if len(name) > 75:
            name = name[:75] + "..."

        # get tracing instance
        telemetry_emit(
            TelemetryEvent(
                name=TraceTaskName.GENERATE_NAME_TRACE,
                context=TelemetryContext(tenant_id=tenant_id, app_id=app_id),
                payload={
                    "conversation_id": conversation_id,
                    "generate_conversation_name": name,
                    "inputs": prompt,
                    "timer": timer,
                    "tenant_id": tenant_id,
                },
            )
        )

        return name

    @classmethod
    def generate_suggested_questions_after_answer(cls, tenant_id: str, histories: str) -> Sequence[str]:
        output_parser = SuggestedQuestionsAfterAnswerOutputParser()
        format_instructions = output_parser.get_format_instructions()

        prompt_template = PromptTemplateParser(template="{{histories}}\n{{format_instructions}}\nquestions:\n")

        prompt = prompt_template.format({"histories": histories, "format_instructions": format_instructions})

        try:
            model_manager = ModelManager()
            model_instance = model_manager.get_default_model_instance(
                tenant_id=tenant_id,
                model_type=ModelType.LLM,
            )
        except InvokeAuthorizationError:
            return []

        prompt_messages = [UserPromptMessage(content=prompt)]

        questions: Sequence[str] = []

        try:
            response: LLMResult = model_instance.invoke_llm(
                prompt_messages=list(prompt_messages),
                model_parameters={
                    "max_tokens": SUGGESTED_QUESTIONS_MAX_TOKENS,
                    "temperature": SUGGESTED_QUESTIONS_TEMPERATURE,
                },
                stream=False,
            )

            text_content = response.message.get_text_content()
            questions = output_parser.parse(text_content) if text_content else []
        except InvokeError:
            questions = []
        except Exception:
            logger.exception("Failed to generate suggested questions after answer")
            questions = []

        return questions

    @classmethod
    def generate_rule_config(
        cls,
        tenant_id: str,
        instruction: str,
        model_config: dict,
        no_variable: bool,
        user_id: str | None = None,
        app_id: str | None = None,
    ):
        output_parser = RuleConfigGeneratorOutputParser()

        error = ""
        error_step = ""
        rule_config = {"prompt": "", "variables": [], "opening_statement": "", "error": ""}
        model_parameters = model_config.get("completion_params", {})
        if no_variable:
            prompt_template = PromptTemplateParser(WORKFLOW_RULE_CONFIG_PROMPT_GENERATE_TEMPLATE)

            prompt_generate = prompt_template.format(
                inputs={
                    "TASK_DESCRIPTION": instruction,
                },
                remove_template_variables=False,
            )

            prompt_messages = [UserPromptMessage(content=prompt_generate)]

            model_manager = ModelManager()

            model_instance = model_manager.get_model_instance(
                tenant_id=tenant_id,
                model_type=ModelType.LLM,
                provider=model_config.get("provider", ""),
                model=model_config.get("name", ""),
            )

            llm_result = None
            with measure_time() as timer:
                try:
                    llm_result = model_instance.invoke_llm(
                        prompt_messages=list(prompt_messages), model_parameters=model_parameters, stream=False
                    )

                    rule_config["prompt"] = cast(str, llm_result.message.content)

                except InvokeError as e:
                    error = str(e)
                    error_step = "generate rule config"
                except Exception as e:
                    logger.exception("Failed to generate rule config, model: %s", model_config.get("name"))
                    rule_config["error"] = str(e)
                    error = str(e)

            rule_config["error"] = f"Failed to {error_step}. Error: {error}" if error else ""

            if user_id:
                prompt_value = rule_config.get("prompt", "")
                generated_output = str(prompt_value) if prompt_value else ""
                cls._emit_prompt_generation_trace(
                    tenant_id=tenant_id,
                    user_id=user_id,
                    app_id=app_id,
                    operation_type=OperationType.RULE_GENERATE,
                    instruction=instruction,
                    generated_output=generated_output,
                    llm_result=llm_result,
                    model_config=model_config,
                    timer=timer,
                    error=error or None,
                )

            return rule_config

        # get rule config prompt, parameter and statement
        prompt_generate, parameter_generate, statement_generate = output_parser.get_format_instructions()

        prompt_template = PromptTemplateParser(prompt_generate)

        parameter_template = PromptTemplateParser(parameter_generate)

        statement_template = PromptTemplateParser(statement_generate)

        # format the prompt_generate_prompt
        prompt_generate_prompt = prompt_template.format(
            inputs={
                "TASK_DESCRIPTION": instruction,
            },
            remove_template_variables=False,
        )
        prompt_messages = [UserPromptMessage(content=prompt_generate_prompt)]

        # get model instance
        model_manager = ModelManager()
        model_instance = model_manager.get_model_instance(
            tenant_id=tenant_id,
            model_type=ModelType.LLM,
            provider=model_config.get("provider", ""),
            model=model_config.get("name", ""),
        )

        llm_result = None
        with measure_time() as timer:
            try:
                try:
                    # the first step to generate the task prompt
                    prompt_content: LLMResult = model_instance.invoke_llm(
                        prompt_messages=list(prompt_messages), model_parameters=model_parameters, stream=False
                    )
                    llm_result = prompt_content
                except InvokeError as e:
                    error = str(e)
                    error_step = "generate prefix prompt"
                    rule_config["error"] = f"Failed to {error_step}. Error: {error}" if error else ""

                    if user_id:
                        cls._emit_prompt_generation_trace(
                            tenant_id=tenant_id,
                            user_id=user_id,
                            app_id=app_id,
                            operation_type=OperationType.RULE_GENERATE,
                            instruction=instruction,
                            generated_output="",
                            llm_result=llm_result,
                            model_config=model_config,
                            timer=timer,
                            error=error,
                        )

                    return rule_config

                rule_config["prompt"] = cast(str, prompt_content.message.content)

                if not isinstance(prompt_content.message.content, str):
                    raise NotImplementedError("prompt content is not a string")
                parameter_generate_prompt = parameter_template.format(
                    inputs={
                        "INPUT_TEXT": prompt_content.message.content,
                    },
                    remove_template_variables=False,
                )
                parameter_messages = [UserPromptMessage(content=parameter_generate_prompt)]

                # the second step to generate the task_parameter and task_statement
                statement_generate_prompt = statement_template.format(
                    inputs={
                        "TASK_DESCRIPTION": instruction,
                        "INPUT_TEXT": prompt_content.message.content,
                    },
                    remove_template_variables=False,
                )
                statement_messages = [UserPromptMessage(content=statement_generate_prompt)]

                try:
                    parameter_content: LLMResult = model_instance.invoke_llm(
                        prompt_messages=list(parameter_messages), model_parameters=model_parameters, stream=False
                    )
                    rule_config["variables"] = re.findall(
                        r'"\s*([^"]+)\s*"', cast(str, parameter_content.message.content)
                    )
                except InvokeError as e:
                    error = str(e)
                    error_step = "generate variables"

                try:
                    statement_content: LLMResult = model_instance.invoke_llm(
                        prompt_messages=list(statement_messages), model_parameters=model_parameters, stream=False
                    )
                    rule_config["opening_statement"] = cast(str, statement_content.message.content)
                except InvokeError as e:
                    error = str(e)
                    error_step = "generate conversation opener"

            except Exception as e:
                logger.exception("Failed to generate rule config, model: %s", model_config.get("name"))
                rule_config["error"] = str(e)
                error = str(e)

        rule_config["error"] = f"Failed to {error_step}. Error: {error}" if error else ""

        if user_id:
            generated_output = rule_config.get("prompt", "")
            cls._emit_prompt_generation_trace(
                tenant_id=tenant_id,
                user_id=user_id,
                app_id=app_id,
                operation_type=OperationType.RULE_GENERATE,
                instruction=instruction,
                generated_output=str(generated_output) if generated_output else "",
                llm_result=llm_result,
                model_config=model_config,
                timer=timer,
                error=error or None,
            )

        return rule_config

    @classmethod
    def generate_code(
        cls,
        tenant_id: str,
        instruction: str,
        model_config: dict,
        code_language: str = "javascript",
        user_id: str | None = None,
        app_id: str | None = None,
    ):
        if code_language == "python":
            prompt_template = PromptTemplateParser(PYTHON_CODE_GENERATOR_PROMPT_TEMPLATE)
        else:
            prompt_template = PromptTemplateParser(JAVASCRIPT_CODE_GENERATOR_PROMPT_TEMPLATE)

        prompt = prompt_template.format(
            inputs={
                "INSTRUCTION": instruction,
                "CODE_LANGUAGE": code_language,
            },
            remove_template_variables=False,
        )

        model_manager = ModelManager()
        model_instance = model_manager.get_model_instance(
            tenant_id=tenant_id,
            model_type=ModelType.LLM,
            provider=model_config.get("provider", ""),
            model=model_config.get("name", ""),
        )

        prompt_messages = [UserPromptMessage(content=prompt)]
        model_parameters = model_config.get("completion_params", {})

        llm_result = None
        error = None
        with measure_time() as timer:
            try:
                llm_result = model_instance.invoke_llm(
                    prompt_messages=list(prompt_messages), model_parameters=model_parameters, stream=False
                )

                generated_code = cast(str, llm_result.message.content)
                result = {"code": generated_code, "language": code_language, "error": ""}

            except InvokeError as e:
                error = str(e)
                result = {"code": "", "language": code_language, "error": f"Failed to generate code. Error: {error}"}
            except Exception as e:
                logger.exception(
                    "Failed to invoke LLM model, model: %s, language: %s", model_config.get("name"), code_language
                )
                error = str(e)
                result = {"code": "", "language": code_language, "error": f"An unexpected error occurred: {str(e)}"}

        if user_id:
            cls._emit_prompt_generation_trace(
                tenant_id=tenant_id,
                user_id=user_id,
                app_id=app_id,
                operation_type=OperationType.CODE_GENERATE,
                instruction=instruction,
                generated_output=result.get("code", ""),
                llm_result=llm_result,
                model_config=model_config,
                timer=timer,
                error=error,
            )

        return result

    @classmethod
    def generate_qa_document(cls, tenant_id: str, query, document_language: str):
        prompt = GENERATOR_QA_PROMPT.format(language=document_language)

        model_manager = ModelManager()
        model_instance = model_manager.get_default_model_instance(
            tenant_id=tenant_id,
            model_type=ModelType.LLM,
        )

        prompt_messages: list[PromptMessage] = [SystemPromptMessage(content=prompt), UserPromptMessage(content=query)]

        # Explicitly use the non-streaming overload
        result = model_instance.invoke_llm(
            prompt_messages=prompt_messages,
            model_parameters={"temperature": 0.01, "max_tokens": 2000},
            stream=False,
        )

        # Runtime type check since pyright has issues with the overload
        if not isinstance(result, LLMResult):
            raise TypeError("Expected LLMResult when stream=False")
        response = result

        answer = cast(str, response.message.content)
        return answer.strip()

    @classmethod
    def generate_structured_output(
        cls, tenant_id: str, instruction: str, model_config: dict, user_id: str | None = None, app_id: str | None = None
    ):
        model_manager = ModelManager()
        model_instance = model_manager.get_model_instance(
            tenant_id=tenant_id,
            model_type=ModelType.LLM,
            provider=model_config.get("provider", ""),
            model=model_config.get("name", ""),
        )

        prompt_messages = [
            SystemPromptMessage(content=SYSTEM_STRUCTURED_OUTPUT_GENERATE),
            UserPromptMessage(content=instruction),
        ]
        model_parameters = model_config.get("model_parameters", {})

        llm_result = None
        error = None
        result = {"output": "", "error": ""}

        with measure_time() as timer:
            try:
                llm_result = model_instance.invoke_llm(
                    prompt_messages=list(prompt_messages), model_parameters=model_parameters, stream=False
                )

                raw_content = llm_result.message.content

                if not isinstance(raw_content, str):
                    raise ValueError(f"LLM response content must be a string, got: {type(raw_content)}")

                try:
                    parsed_content = json.loads(raw_content)
                except json.JSONDecodeError:
                    parsed_content = json_repair.loads(raw_content)

                if not isinstance(parsed_content, dict | list):
                    raise ValueError(f"Failed to parse structured output from llm: {raw_content}")

                generated_json_schema = json.dumps(parsed_content, indent=2, ensure_ascii=False)
                result = {"output": generated_json_schema, "error": ""}

            except InvokeError as e:
                error = str(e)
                result = {"output": "", "error": f"Failed to generate JSON Schema. Error: {error}"}
            except Exception as e:
                logger.exception("Failed to invoke LLM model, model: %s", model_config.get("name"))
                error = str(e)
                result = {"output": "", "error": f"An unexpected error occurred: {str(e)}"}

        if user_id:
            cls._emit_prompt_generation_trace(
                tenant_id=tenant_id,
                user_id=user_id,
                app_id=app_id,
                operation_type=OperationType.STRUCTURED_OUTPUT,
                instruction=instruction,
                generated_output=result.get("output", ""),
                llm_result=llm_result,
                model_config=model_config,
                timer=timer,
                error=error,
            )

        return result

    @staticmethod
    def instruction_modify_legacy(
        tenant_id: str,
        flow_id: str,
        current: str,
        instruction: str,
        model_config: dict,
        ideal_output: str | None,
        user_id: str | None = None,
        app_id: str | None = None,
    ):
        last_run: Message | None = (
            db.session.query(Message).where(Message.app_id == flow_id).order_by(Message.created_at.desc()).first()
        )
        if not last_run:
            result = LLMGenerator.__instruction_modify_common(
                tenant_id=tenant_id,
                model_config=model_config,
                last_run=None,
                current=current,
                error_message="",
                instruction=instruction,
                node_type="llm",
                ideal_output=ideal_output,
                user_id=user_id,
                app_id=app_id,
            )
        else:
            last_run_dict = {
                "query": last_run.query,
                "answer": last_run.answer,
                "error": last_run.error,
            }
            result = LLMGenerator.__instruction_modify_common(
                tenant_id=tenant_id,
                model_config=model_config,
                last_run=last_run_dict,
                current=current,
                error_message=str(last_run.error),
                instruction=instruction,
                node_type="llm",
                ideal_output=ideal_output,
                user_id=user_id,
                app_id=app_id,
            )
        return result

    @staticmethod
    def instruction_modify_workflow(
        tenant_id: str,
        flow_id: str,
        node_id: str,
        current: str,
        instruction: str,
        model_config: dict,
        ideal_output: str | None,
        workflow_service: WorkflowServiceInterface,
        user_id: str | None = None,
        app_id: str | None = None,
    ):
        session = db.session()

        app: App | None = session.query(App).where(App.id == flow_id).first()
        if not app:
            raise ValueError("App not found.")
        workflow = workflow_service.get_draft_workflow(app_model=app)
        if not workflow:
            raise ValueError("Workflow not found for the given app model.")
        last_run = workflow_service.get_node_last_run(app_model=app, workflow=workflow, node_id=node_id)
        try:
            node_type = cast(WorkflowNodeExecutionModel, last_run).node_type
        except Exception:
            try:
                node_type = [it for it in workflow.graph_dict["graph"]["nodes"] if it["id"] == node_id][0]["data"][
                    "type"
                ]
            except Exception:
                node_type = "llm"

        if not last_run:  # Node is not executed yet
            return LLMGenerator.__instruction_modify_common(
                tenant_id=tenant_id,
                model_config=model_config,
                last_run=None,
                current=current,
                error_message="",
                instruction=instruction,
                node_type=node_type,
                ideal_output=ideal_output,
                user_id=user_id,
                app_id=app_id,
            )

        def agent_log_of(node_execution: WorkflowNodeExecutionModel) -> Sequence:
            raw_agent_log = node_execution.execution_metadata_dict.get(WorkflowNodeExecutionMetadataKey.AGENT_LOG, [])
            if not raw_agent_log:
                return []

            return [
                {
                    "status": event["status"],
                    "error": event["error"],
                    "data": event["data"],
                }
                for event in raw_agent_log
            ]

        inputs = last_run.load_full_inputs(session, storage)
        last_run_dict = {
            "inputs": inputs,
            "status": last_run.status,
            "error": last_run.error,
            "agent_log": agent_log_of(last_run),
        }

        return LLMGenerator.__instruction_modify_common(
            tenant_id=tenant_id,
            model_config=model_config,
            last_run=last_run_dict,
            current=current,
            error_message=last_run.error,
            instruction=instruction,
            node_type=last_run.node_type,
            ideal_output=ideal_output,
            user_id=user_id,
            app_id=app_id,
        )

    @staticmethod
    def __instruction_modify_common(
        tenant_id: str,
        model_config: dict,
        last_run: dict | None,
        current: str | None,
        error_message: str | None,
        instruction: str,
        node_type: str,
        ideal_output: str | None,
        user_id: str | None = None,
        app_id: str | None = None,
    ):
        LAST_RUN = "{{#last_run#}}"
        CURRENT = "{{#current#}}"
        ERROR_MESSAGE = "{{#error_message#}}"
        injected_instruction = instruction
        if LAST_RUN in injected_instruction:
            injected_instruction = injected_instruction.replace(LAST_RUN, json.dumps(last_run))
        if CURRENT in injected_instruction:
            injected_instruction = injected_instruction.replace(CURRENT, current or "null")
        if ERROR_MESSAGE in injected_instruction:
            injected_instruction = injected_instruction.replace(ERROR_MESSAGE, error_message or "null")
        model_instance = ModelManager().get_model_instance(
            tenant_id=tenant_id,
            model_type=ModelType.LLM,
            provider=model_config.get("provider", ""),
            model=model_config.get("name", ""),
        )
        match node_type:
            case "llm" | "agent":
                system_prompt = LLM_MODIFY_PROMPT_SYSTEM
            case "code":
                system_prompt = LLM_MODIFY_CODE_SYSTEM
            case _:
                system_prompt = LLM_MODIFY_PROMPT_SYSTEM
        prompt_messages = [
            SystemPromptMessage(content=system_prompt),
            UserPromptMessage(
                content=json.dumps(
                    {
                        "current": current,
                        "last_run": last_run,
                        "instruction": injected_instruction,
                        "ideal_output": ideal_output,
                    }
                )
            ),
        ]
        model_parameters = {"temperature": 0.4}

        llm_result = None
        error = None
        result = {}

        with measure_time() as timer:
            try:
                llm_result = model_instance.invoke_llm(
                    prompt_messages=list(prompt_messages), model_parameters=model_parameters, stream=False
                )

                generated_raw = llm_result.message.get_text_content()
                first_brace = generated_raw.find("{")
                last_brace = generated_raw.rfind("}")
                if first_brace == -1 or last_brace == -1 or last_brace < first_brace:
                    raise ValueError(f"Could not find a valid JSON object in response: {generated_raw}")
                json_str = generated_raw[first_brace : last_brace + 1]
                data = json_repair.loads(json_str)
                if not isinstance(data, dict):
                    raise TypeError(f"Expected a JSON object, but got {type(data).__name__}")
                result = data
            except InvokeError as e:
                error = str(e)
                result = {"error": f"Failed to generate code. Error: {error}"}
            except Exception as e:
                logger.exception(
                    "Failed to invoke LLM model, model: %s", json.dumps(model_config.get("name")), exc_info=True
                )
                error = str(e)
                result = {"error": f"An unexpected error occurred: {str(e)}"}

        if user_id:
            generated_output = ""
            if isinstance(result, dict):
                for key in ["prompt", "code", "output", "modified"]:
                    if result.get(key):
                        generated_output = str(result[key])
                        break

            LLMGenerator._emit_prompt_generation_trace(
                tenant_id=tenant_id,
                user_id=user_id,
                app_id=app_id,
                operation_type=OperationType.INSTRUCTION_MODIFY,
                instruction=instruction,
                generated_output=generated_output,
                llm_result=llm_result,
                model_config=model_config,
                timer=timer,
                error=error,
            )

        return result

    @classmethod
    def _emit_prompt_generation_trace(
        cls,
        tenant_id: str,
        user_id: str,
        app_id: str | None,
        operation_type: OperationType,
        instruction: str,
        generated_output: str,
        llm_result: LLMResult | None,
        model_config: dict | None = None,
        timer=None,
        error: str | None = None,
    ):
        if llm_result:
            prompt_tokens = llm_result.usage.prompt_tokens
            completion_tokens = llm_result.usage.completion_tokens
            total_tokens = llm_result.usage.total_tokens
            model_name = llm_result.model
            # Extract provider from model_config if available, otherwise fall back to parsing model name
            if model_config and model_config.get("provider"):
                model_provider = model_config.get("provider", "")
            else:
                model_provider = model_name.split("/")[0] if "/" in model_name else ""
            latency = llm_result.usage.latency
            total_price = float(llm_result.usage.total_price) if llm_result.usage.total_price else None
            currency = llm_result.usage.currency
        else:
            prompt_tokens = 0
            completion_tokens = 0
            total_tokens = 0
            model_provider = model_config.get("provider", "") if model_config else ""
            model_name = model_config.get("name", "") if model_config else ""
            latency = 0.0
            if timer:
                start_time = timer.get("start")
                end_time = timer.get("end")
                if start_time and end_time:
                    latency = (end_time - start_time).total_seconds()
            total_price = None
            currency = None

        telemetry_emit(
            TelemetryEvent(
                name=TraceTaskName.PROMPT_GENERATION_TRACE,
                context=TelemetryContext(tenant_id=tenant_id, user_id=user_id, app_id=app_id),
                payload={
                    "tenant_id": tenant_id,
                    "user_id": user_id,
                    "app_id": app_id,
                    "operation_type": operation_type,
                    "instruction": instruction,
                    "generated_output": generated_output,
                    "prompt_tokens": prompt_tokens,
                    "completion_tokens": completion_tokens,
                    "total_tokens": total_tokens,
                    "model_provider": model_provider,
                    "model_name": model_name,
                    "latency": latency,
                    "total_price": total_price,
                    "currency": currency,
                    "timer": timer,
                    "error": error,
                },
            )
        )
