daat-locus 0.1.1

A long-running local agent runtime with memory, workflows, apps, and sleep-time self-improvement.
use schemars::schema_for;

use crate::context::Context;
use crate::reasoning::{
    examples::ProgramExample,
    ir::PromptIR,
    optimizer::PromptTuningConfig,
    program::Program,
    prompt_doc::{PromptBlock, PromptDocument, PromptNode, PromptStateDoc},
    prompt_renderer::LlmPromptRenderer,
    prompt_text::{PromptTextBuilder, render_bullet_list},
    runtime::{HistoryMessage, PromptRequest},
    signature::Signature,
};
use crate::schema_utils::normalize_openai_json_schema;

use super::Renderer;

pub struct OpenAIToolRenderer;

impl Renderer for OpenAIToolRenderer {
    fn render<P: Program>(
        &self,
        context: &Context,
        program: &P,
        mut ir: PromptIR,
        tuning: &PromptTuningConfig<P::Output>,
    ) -> PromptRequest {
        let signature = program.signature();
        let examples = if tuning.examples.is_empty() {
            program.examples()
        } else {
            tuning.examples.clone()
        };

        for instruction in &tuning.extra_instructions {
            ir.instructions.push(instruction.clone());
        }

        let mut user_message = PromptTextBuilder::new();
        user_message.push_markdown_section("Program Signature", render_signature_block(&signature));
        if !examples.is_empty() {
            user_message.push_markdown_section("Examples", render_examples_block(&examples));
        }
        if !ir.instructions.is_empty() {
            user_message
                .push_labeled_section("Task Instructions", render_bullet_list(ir.instructions));
        }
        for section in ir.sections {
            user_message.push_markdown_section(section.title, section.body);
        }

        PromptRequest {
            tool_name: program.name().to_string(),
            tool_description: program.description().to_string(),
            output_schema: normalize_openai_json_schema(
                serde_json::to_value(schema_for!(P::Output)).unwrap(),
            ),
            system_messages: ir.system,
            long_term_memory_messages: if program.include_long_term_memory_messages() {
                build_long_term_memory_messages(context)
            } else {
                Vec::new()
            },
            history_messages: if program.include_history_messages() {
                context.memory.runtime_conversation_messages()
            } else {
                Vec::new()
            },
            current_user_message: user_message.build(),
            retry_messages: Vec::new(),
        }
    }
}

fn build_long_term_memory_messages(context: &Context) -> Vec<HistoryMessage> {
    if context.prompt_memory.is_empty() {
        return Vec::new();
    }

    let mut children = Vec::new();
    if !context.prompt_memory.observations.is_empty() {
        children.push(PromptNode::State(PromptStateDoc::new(
            "observations",
            context
                .prompt_memory
                .observations
                .iter()
                .map(|fact| {
                    PromptBlock::Paragraph(format!(
                        "id: {}\ntype: {}\ntext: {}",
                        fact.id,
                        fact.memory_type
                            .clone()
                            .unwrap_or_else(|| "observation".to_string()),
                        fact.text.trim()
                    ))
                })
                .collect(),
        )));
    }
    if !context.prompt_memory.raw_memories.is_empty() {
        children.push(PromptNode::State(PromptStateDoc::new(
            "raw_memories",
            context
                .prompt_memory
                .raw_memories
                .iter()
                .map(|fact| {
                    PromptBlock::Paragraph(format!(
                        "id: {}\ntype: {}\ntext: {}",
                        fact.id,
                        fact.memory_type
                            .clone()
                            .unwrap_or_else(|| "memory".to_string()),
                        fact.text.trim()
                    ))
                })
                .collect(),
        )));
    }
    if !context.prompt_memory.citations.is_empty() {
        children.push(PromptNode::State(PromptStateDoc::new(
            "citations",
            vec![PromptBlock::BulletList(
                context
                    .prompt_memory
                    .citations
                    .iter()
                    .map(|citation| {
                        format!("[{}] {}: {}", citation.kind, citation.id, citation.summary)
                    })
                    .collect(),
            )],
        )));
    }

    let doc = PromptDocument::new(vec![PromptNode::Group(
        super::super::prompt_doc::PromptGroupDoc::new("recall_memories", children),
    )]);

    LlmPromptRenderer::render_system_messages(&doc)
        .into_iter()
        .map(HistoryMessage::system)
        .collect()
}

fn render_signature_block(signature: &Signature) -> String {
    let mut builder = PromptTextBuilder::new();
    builder.push_labeled_section("Objective", signature.objective.clone());
    if !signature.inputs.is_empty() {
        builder.push_bullet_list_section(
            "Input Signature",
            signature
                .inputs
                .iter()
                .map(|field| format!("{}: {}", field.name, field.description)),
        );
    }
    if !signature.outputs.is_empty() {
        builder.push_bullet_list_section(
            "Output Signature",
            signature
                .outputs
                .iter()
                .map(|field| format!("{}: {}", field.name, field.description)),
        );
    }
    if !signature.rules.is_empty() {
        builder.push_bullet_list_section("Signature Rules", signature.rules.clone());
    }
    builder.build()
}

fn render_examples_block<O: serde::Serialize>(examples: &[ProgramExample<O>]) -> String {
    let sections = examples
        .iter()
        .enumerate()
        .map(|(index, example)| {
            let mut body = PromptTextBuilder::new();
            body.push_paragraph(format!("### Example {}\n{}", index + 1, example.title));
            if !example.inputs.is_empty() {
                body.push_bullet_list_section(
                    "Inputs",
                    example
                        .inputs
                        .iter()
                        .map(|field| format!("{}: {}", field.name, field.value)),
                );
            }
            body.push_labeled_section(
                "Output (JSON)",
                format!(
                    "```json\n{}\n```",
                    serde_json::to_string_pretty(&example.output).unwrap()
                ),
            );
            body.build()
        })
        .collect::<Vec<_>>();

    sections.join("\n\n")
}