import * as i0 from '@angular/core';
import { inject, Injectable } from '@angular/core';
import * as i1 from '@c8y/client';
import { FetchClient } from '@c8y/client';
import { Observable } from 'rxjs';
import { gettext } from '@c8y/ngx-components/gettext';

// Internal implementation detail; maybe we don't even need this enum since
var DataStreamType;
(function (DataStreamType) {
    DataStreamType["TEXT_DELTA"] = "text-delta";
    DataStreamType["TOOL_CALL"] = "tool-call";
    DataStreamType["TOOL_INPUT_START"] = "tool-input-start";
    DataStreamType["TOOL_INPUT_DELTA"] = "tool-input-delta";
    DataStreamType["TOOL_CALL_STREAMING"] = "tool-call-streaming";
    DataStreamType["TOOL_CALL_DELTA"] = "tool-call-delta";
    DataStreamType["TOOL_RESULT"] = "tool-result";
    DataStreamType["REASONING"] = "reasoning";
    DataStreamType["REASONING_DELTA"] = "reasoning-delta";
    DataStreamType["REDACTED_REASONING"] = "redacted-reasoning";
    DataStreamType["REASONING_SIGNATURE"] = "reasoning-signature";
    DataStreamType["FINISH_REASONING"] = "finish-reasoning";
    DataStreamType["FINISH"] = "finish";
    DataStreamType["FINISH_STEP"] = "finish-step";
    DataStreamType["ERROR"] = "error";
    DataStreamType["DATA"] = "data";
    DataStreamType["MESSAGE_ANNOTATIONS"] = "message-annotations";
    DataStreamType["SOURCE"] = "source";
    DataStreamType["FILE"] = "file";
    DataStreamType["STEP_START"] = "start-step";
    DataStreamType["STEP_FINISH"] = "finish-step";
})(DataStreamType || (DataStreamType = {}));
/** This service manages communication with the AI Agent Manager microservice, supporting
 * management of agents, and sending requests to the agents.
 */
class AIService {
    // nb: allowing the client to be passed in the constructor allows efficient unit testing without setting up the Angular TestBed
    constructor(client) {
        this.baseUrl = '/service/ai';
        this.client = client ?? inject(FetchClient);
    }
    /**
     * Creates or updates an agent.
     * @param def The agent definition
     */
    async createOrUpdateAgent(def) {
        const health = await this.getAgentHealth(def.name);
        let resource = `${this.baseUrl}/agent/${def.type}`;
        let method = 'POST';
        if (health.exists) {
            resource = `${this.baseUrl}/agent/${def.type}/${def.name}`;
            method = 'PUT';
        }
        const response = await this.client.fetch(resource, {
            body: JSON.stringify(def),
            method,
            headers: { 'Content-Type': 'application/json' }
        });
        if (!response.ok) {
            throw new Error(`Failed to create agent: ${response.statusText}`);
        }
    }
    /**
     * Check if an agent exists and is ready for use.
     * @param name Agent name (optional)
     * @param fromApp The app context path to check for an agent. This can be used, if the agent should get distributed by the plugin (optional).
     * @returns Agent health check response.
     */
    async getAgentHealth(name = '', fromApp) {
        const response = await this.client.fetch(`${this.baseUrl}/agent/test/${name || crypto.randomUUID()}${fromApp ? `?fromApp=${fromApp}` : ''}`, {
            method: 'GET',
            headers: { 'Content-Type': 'application/json' }
        });
        if (!response.ok) {
            return {
                status: response.status === 403 ? 'missing-permissions' : 'missing-microservice',
                exists: false,
                canCreate: false,
                isProviderConfigured: false,
                messages: response.status === 403 ? [] : [response.statusText]
            };
        }
        const json = await response.json();
        if (!json.isProviderConfigured) {
            json.status = 'missing-provider';
        }
        else if (!json.exists) {
            json.status = 'missing-agent';
        }
        else {
            json.status = 'ready';
        }
        return json;
    }
    /**
     * Send a text message to the agent.
     * @param name Agent name
     * @param messages The message to send, including any previous message history.
     * Typically you should use `defaultPruneMessagesForAgent` or a custom function to prepare the message history
     * by removing unnecessary content such as large/old tool inputs/outputs.
     * @param variables Variables to include
     * @param fromApp The app context path to check for an agent. This can be used, if the agent should get distributed by the plugin (optional).
     * @returns Text response from the agent.
     */
    async text(name, messages, variables, fromApp) {
        const parsedMessages = this.convertToAgentMessageFormat(messages);
        const data = this.client.fetch(`${this.baseUrl}/agent/text/${name}${fromApp ? `?fromApp=${fromApp}` : ''}`, {
            body: JSON.stringify({ messages: parsedMessages, variables }),
            method: 'POST',
            headers: { 'Content-Type': 'application/json' }
        });
        const response = await data;
        if (!response.ok) {
            throw new Error(`Failed to talk with agent: ${response.statusText}`);
        }
        const text = await response.text();
        return text;
    }
    /**
     * Send messages to an object-type agent via the non-streaming test endpoint.
     * Object agents return a single structured JSON response rather than a stream.
     *
     * @param agent A ClientAgentDefinition with `snapshot: true` (required for test endpoint).
     * @param messages Messages to send
     * @param variables Variables to include
     * @param abortController An AbortController to cancel the request.
     * @returns The full response including the structured object and usage information.
     */
    async callObjectAgent(agent, messages, variables, abortController) {
        const parsedMessages = this.convertToAgentMessageFormat(messages);
        const agentName = typeof agent === 'string' ? agent : agent.definition.name;
        const useTest = typeof agent !== 'string' && agent.snapshot;
        const baseUrl = useTest
            ? `${this.baseUrl}/agent/test/${typeof agent !== 'string' ? agent.definition.type : 'object'}`
            : `${this.baseUrl}/agent/${typeof agent !== 'string' ? agent.definition.type : 'object'}/${agentName}`;
        let body = JSON.stringify({ messages: parsedMessages, variables });
        if (useTest) {
            const agentCopy = {
                ...agent.definition
            };
            agentCopy.agent.messages = parsedMessages;
            agentCopy.agent.variables = variables;
            body = JSON.stringify(agentCopy);
        }
        const response = await this.client.fetch(`${baseUrl}?fullResponse=true`, {
            method: 'POST',
            body,
            headers: {
                ...this.client.defaultHeaders,
                'content-type': 'application/json'
            },
            signal: abortController.signal
        });
        if (response.status > 300) {
            const data = await response.json();
            throw new Error(JSON.stringify(data, null, 2));
        }
        const data = await response.json();
        if (!('object' in data)) {
            throw new Error('Invalid response: missing required "object" field');
        }
        if (typeof data.object !== 'object' || data.object === null) {
            throw new Error('Invalid response: "object" field must be an object');
        }
        // Validate optional totalUsage field if present
        if (data.totalUsage !== undefined) {
            if (typeof data.totalUsage !== 'object' || data.totalUsage === null) {
                throw new Error('Invalid response: "totalUsage" must be an object');
            }
        }
        return data;
    }
    /**
     * Stream a text message to the agent.
     * @param agent Agent name or an agent definition.
     * @param messages The message to send, including any previous message history.
     * Typically you should use `defaultPruneMessagesForAgent` or a custom function to prepare the message history
     * by removing unnecessary content such as large/old tool inputs/outputs.
     * @param variables Variables to include
     * @param abortController An AbortController to cancel the request.
     * @param fromApp The app context path to check for an agent. This can be used, if the agent should get distributed by the plugin (optional).
     * @returns An observable that emits AIStreamResponse including partial `AIMessage` objects as they are received.
     * When additional useful metadata is available, this will be streamed with type indicating it is metadata.
     * The observable can be cancelled using the provided AbortController.
     * The observable will emit an error if the request fails or is aborted.
     * The observable will complete when the stream is finished.
     *
     * The messages sent to the agent can include special options:
     * - `hiddenContent`: If set, this content will be sent to the agent instead of the `content` field.
     * - `skipToLLM`: If set to true, this message will be skipped when sending to the agent.
     *
     * Example usage:
     * ```typescript
     * const abortController = new AbortController();
     * const messages: AIMessage[] = [
     *   { role: 'user', content: 'Hello' },
     *   { role: 'assistant', content: 'Hi there!' },
     *   { role: 'user', content: 'Tell me a joke.', options: { hiddenContent: 'Tell me a joke about cats.' } }
     * ];
     * const observable = aiService.stream$('my-agent', messages, {}, abortController);
     * const subscription = observable.subscribe({
     *   next: (response) => { console.log('Received message update:', response.message); },
     *   error: (err) => console.error('Error:', err),
     *   complete: () => console.log('Stream complete')
     * });
     *
     * // To cancel the request:
     * abortController.abort();
     * subscription.unsubscribe();
     * ```
     */
    async stream$(agent, messages, variables, abortController, fromApp) {
        const parsedMessages = this.convertToAgentMessageFormat(messages);
        const agentName = typeof agent === 'string' ? agent : agent.definition.name;
        const useTest = typeof agent !== 'string' && agent.snapshot;
        const baseUrl = useTest
            ? `${this.baseUrl}/agent/test/text`
            : `${this.baseUrl}/agent/text/${agentName}`;
        let body = JSON.stringify({ messages: parsedMessages, variables });
        if (useTest) {
            const agentCopy = {
                ...agent.definition
            };
            agentCopy.agent['messages'] = parsedMessages;
            agentCopy.agent['variables'] = variables;
            body = JSON.stringify(agentCopy);
        }
        const response = await this.client.fetch(`${baseUrl}?fullResponse=true${fromApp ? `&fromApp=${fromApp}` : ''}`, {
            method: 'POST',
            body,
            headers: {
                ...this.client.defaultHeaders,
                'content-type': 'application/json',
                accept: 'text/event-stream'
            },
            signal: abortController.signal
        });
        const stream = response.body;
        const decoder = new TextDecoder();
        return new Observable(observer => {
            if (response.status > 300) {
                response
                    .json()
                    .then(data => {
                    observer.error(`Error communicating with server: ${JSON.stringify(data, null, 2)}`);
                })
                    .catch(err => {
                    observer.error(`Error communicating with server: HTTP ${response.status}: ${response.statusText}; ${err}`);
                });
                return;
            }
            if (!stream) {
                observer.error('Server error - no response body');
                return;
            }
            const reader = stream.getReader();
            const message = {
                role: 'assistant',
                content: []
            };
            const abortHandler = (e) => {
                const reason = e?.target?.reason || gettext('Aborted');
                reader.cancel();
                observer.error(new DOMException(reason, 'AbortError'));
            };
            abortController.signal.addEventListener('abort', abortHandler);
            let buffer = '';
            const read = () => {
                reader.read().then(({ done, value }) => {
                    if (done) {
                        if (buffer.trim())
                            this.processLine(buffer, observer, message); // process any remaining data
                        observer.complete();
                        return;
                    }
                    buffer += decoder.decode(value, { stream: true });
                    const lines = buffer.split('\n\n');
                    buffer = lines.pop() || '';
                    for (const line of lines) {
                        if (line.trim())
                            this.processLine(line, observer, message);
                    }
                    read();
                });
            };
            read();
            return () => {
                abortController.signal.removeEventListener('abort', abortHandler);
                reader.cancel();
            };
        });
    }
    convertToAgentMessageFormat(messages) {
        // We will need to rewrite this to include tool outputs soon (effectively this means targetting LanguageModelV*ToolResultOutput).
        // Once we have more fields than just role and content, we will need to convert between our format and
        // the Vercel format expected by the agent manager (e.g. steps->content parts)
        return messages.map(m => {
            if (m.role !== 'assistant') {
                return {
                    role: m.role,
                    content: m.content
                };
            }
            // For assistant messages, check if there's an object part
            const objectPart = m.content.find((part) => part.type === 'object');
            if (objectPart) {
                // If there's an object part, parse and send the JSON object as content
                return {
                    role: m.role,
                    content: objectPart.jsonContent
                };
            }
            // Otherwise, send text parts as joined string
            return {
                role: m.role,
                content: m.content
                    .filter((part) => part.type === 'text')
                    .map(part => part.text)
                    .join('\n\n')
            };
        });
    }
    /**
     * Convert a tool output from the MCP/wire format (which is different/more compact than LanguageModelV3ToolResultOutput)
     * into our own standard representation, which can be roundtripped to the format we need to provide when making future requests.
     *
     * Handles different data types from Vercel's streaming API:
     * - Text: stored as simple string in output field
     * - JSON objects: stored as-is in output field
     * - Content arrays: stored in output field with outputType="content" to enable roundtripping
     * - Single text in content array: unwrapped to simple string for simpler handling (and easier truncation)
     * - Errors: error flag set to true
     */
    convertStreamingToolOutputToToolCallPart(output, target) {
        if (!output) {
            return;
        }
        if (output.isError || output.type === 'error-text' || output.type === 'error-json')
            target.error = true;
        // Handle content array format from streaming API
        if (output.content && Array.isArray(output.content)) {
            // Single text element - unwrap to simple string for simplicity
            if (output.content.length === 1 && output.content[0].type === 'text') {
                target.output = output.content[0].text;
                return;
            }
            // Multiple elements or mixed types - preserve as content array
            target.output = output.content;
            target.outputType = 'content'; // Mark as content to enable roundtripping
            return;
        }
        // Plain string or structured (JSON-serializable) object - store as-is
        target.output = output;
    }
    /** Add the specified tool call info to the current step. If this toolCallId is already known,
     * updates it rather than adding a new one, since for UI rendering and compactness of data it's
     * easiest to just have one item per tool call.
     *
     * Returns the changed item.
     */
    addToolCallInfo(type, rawInfo, addTo) {
        // Vercel v4 seems to set this to "id" not "toolCallId" in some places despite doc to the contrary, so normalize it here
        if (!rawInfo.toolCallId)
            rawInfo.toolCallId = rawInfo.id;
        let existing = addTo.find((tc) => tc.type.startsWith('tool-') && tc.toolCallId === rawInfo.toolCallId);
        if (existing) {
            existing.type = type;
        }
        else if (!existing) {
            existing = { type: type, toolName: rawInfo.toolName, toolCallId: rawInfo.toolCallId };
            addTo.push(existing);
        }
        if (rawInfo?.input)
            existing.input = rawInfo.input;
        if (rawInfo?.output)
            this.convertStreamingToolOutputToToolCallPart(rawInfo.output, existing);
        return existing;
    }
    /** Unpacks data from the Vercel Data Stream Protocol (sent by the Agent Manager over SSE) to update the message
     *
     * See streamText().fullStream() from https://ai-sdk.dev/docs/ai-sdk-ui/stream-protocol#data-stream-protocol
     *
     * This is similar to what the Vercel `toUIMessageStreamResponse` API does (though it'd need the agent manager to publish the
     * UIMessageStream which has a bit more - useful - data than what is included here).
     *
     * We always use the same parts object, to make it easier for consumers (eg. agent-chat)
     * to add items to the list and have them still there on the next update from this service.
     */
    processLine(line, observer, message) {
        if (!line.trim()) {
            return;
        }
        try {
            let data = {};
            let type = '';
            try {
                data = JSON.parse(line.replace('data: ', ''));
                type = data.type;
            }
            catch (e) {
                console.error('Error parsing line from AI response: ', line, e);
                return;
            }
            let lastPart = !message.content ? undefined : message.content[message.content.length - 1];
            switch (type) {
                case DataStreamType.STEP_START:
                    if (data.request && data.request.body) {
                        observer.next({
                            message: message,
                            changedPart: {
                                type: 'response-metadata',
                                model: data.request.body.model,
                                systemPrompt: data.request.body.systemPrompt
                            }
                        });
                    }
                    // Don't add consecutive duplicate step bounaries
                    if (!lastPart || lastPart.type !== 'step-start') {
                        message.content.push({ type: 'step-start' });
                        observer.next({ message, changedPart: message.content[message.content.length - 1] });
                    }
                    return;
                case DataStreamType.REASONING_DELTA:
                    const reasoning = data.text;
                    if (!reasoning)
                        return;
                    if (lastPart?.type === 'reasoning') {
                        lastPart.text += reasoning;
                    }
                    else {
                        lastPart = { type: 'reasoning', text: reasoning };
                        message.content.push(lastPart);
                    }
                    observer.next({ message, changedPart: lastPart });
                    return;
                case DataStreamType.TEXT_DELTA:
                    const text = data.textDelta || data.text;
                    if (!text)
                        return;
                    if (lastPart?.type === 'text') {
                        lastPart.text += text;
                    }
                    else {
                        lastPart = { type: 'text', text: text };
                        message.content.push(lastPart);
                    }
                    observer.next({ message, changedPart: lastPart });
                    return;
                case DataStreamType.TOOL_INPUT_START:
                    observer.next({
                        message,
                        changedPart: this.addToolCallInfo('tool-input-streaming', data, message.content)
                    });
                    return;
                case DataStreamType.TOOL_CALL:
                    observer.next({
                        message,
                        changedPart: this.addToolCallInfo('tool-executing', data, message.content)
                    });
                    return;
                case DataStreamType.TOOL_RESULT:
                    observer.next({
                        message,
                        changedPart: this.addToolCallInfo('tool-result', data, message.content)
                    });
                    return;
                case DataStreamType.FINISH:
                    message.finishReason = data.finishReason; // FUTURE could be anything, may not match the options in AIMessage
                    message.usage = data.totalUsage;
                    observer.next({ message, changedPart: undefined });
                    observer.complete();
                    return;
                case DataStreamType.ERROR:
                    message.finishReason = 'error';
                    observer.next({ message, changedPart: undefined });
                    observer.error(data?.message || data?.errorText || data);
                    // Error is instead of completing the stream
                    return;
                // Should we also handle other types such as "abort"?
            }
        }
        catch (e) {
            observer.error(e);
        }
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: AIService, deps: [{ token: i1.FetchClient }], target: i0.ɵɵFactoryTarget.Injectable }); }
    static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: AIService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: AIService, decorators: [{
            type: Injectable,
            args: [{
                    providedIn: 'root'
                }]
        }], ctorParameters: () => [{ type: i1.FetchClient }] });
/**
 * The default implementation of for reducing the size of the message history so it is ready to be sent to the LLM agent.
 *
 * This is important since old tool inputs and outputs can be very large and fill up the agent's context window.
 *
 * Implements `pruneMessagesFunction`.
 *
 * When sending an AI request you can provide your own function instead of using this one,
 * or you can write your own function that calls this and adds extra logic,
 * for example to suppress specific tools from the AIMessage, or
 * retain only the final step text in multi-step responses, etc.
 *
 * The current default implementation preserves text content, but does not include any tool calls.
 * This may change in future releases.
 */
function defaultPruneMessagesForAgent(messages) {
    // When we change this to include tool calls, we should remove large inputs and truncate large and/or old outputs
    // to avoid too many tokens and context window consumption.
    // Some applications will want to entirely remove specific tool calls that have only transient use,
    // which can be done by providing their own pruneMessagesForAgent function.
    return messages.map(m => {
        if (m.role !== 'assistant') {
            return {
                role: m.role,
                content: m.content
            };
        }
        const textParts = m.content.filter((part) => part.type === 'text');
        const objectParts = m.content.filter((part) => part.type === 'object');
        const content = [];
        // Only add text part if there are text parts
        if (textParts.length > 0) {
            content.push({
                type: 'text',
                text: textParts.map(part => part.text).join('\n\n')
            });
        }
        // Preserve object parts for object agents
        content.push(...objectParts);
        return {
            role: m.role,
            content
        };
    });
}

/**
 * Generated bundle index. Do not edit.
 */

export { AIService, DataStreamType, defaultPruneMessagesForAgent };
//# sourceMappingURL=c8y-ngx-components-ai.mjs.map
