diff --git a/dice/src/main/resources/prompts/suggest_entities.jinja b/dice/src/main/resources/prompts/suggest_entities.jinja index 5f9c7a55..f2776751 100644 --- a/dice/src/main/resources/prompts/suggest_entities.jinja +++ b/dice/src/main/resources/prompts/suggest_entities.jinja @@ -1,16 +1,25 @@ You are analyzing sources. -{% if context.directions %} - Consider the following direction: {{ context.directions }} +{# `directions` is a prompt variable, not a context field — SourceAnalysisContext + spreads promptVariables at the top level of the model, so this reads whatever a + caller supplied and stays silent when none did. `context.directions` was a + vestige of a field that no longer exists, so this block never rendered. #} +{% if directions %} + Consider the following direction: {{ directions }} {% endif %} Given the following text, identify and summarize all entities mentioned. Include the entity id only if it's provided in the text as a UUID, not a name. Entity types must only come from the following list: +{# `ownLabel`, not `clazz.simpleName`: `clazz` exists only on JvmType, so every + DynamicType — the schema-declared types, which are most of them in a configured + world — rendered as an empty name in a list the model is told to choose from. + For a JvmType, ownLabel IS the simple name, and it is the key the extracted + labels are matched back against. #} {% for domainType in context.schema.domainTypes %} - - {{ domainType.clazz.simpleName }}: {{ domainType.description }} - Parents: {{ domainType.parents | map(attribute='clazz') | map(attribute='simpleName') | join(', ') }} + - {{ domainType.ownLabel }}: {{ domainType.description }} + Parents: {{ domainType.parents | map(attribute='ownLabel') | join(', ') }} Properties: {% for property in domainType.properties %} - {{ property.name }} ({{ property.cardinality }}): {{ property.description }} diff --git a/dice/src/test/kotlin/com/embabel/dice/entity/SuggestEntitiesTemplateTest.kt b/dice/src/test/kotlin/com/embabel/dice/entity/SuggestEntitiesTemplateTest.kt new file mode 100644 index 00000000..8b54b592 --- /dev/null +++ b/dice/src/test/kotlin/com/embabel/dice/entity/SuggestEntitiesTemplateTest.kt @@ -0,0 +1,102 @@ +/* + * Copyright 2024-2026 Embabel Pty Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.embabel.dice.entity + +import com.embabel.agent.core.Cardinality +import com.embabel.agent.core.ContextId +import com.embabel.agent.core.DataDictionary +import com.embabel.agent.core.DynamicType +import com.embabel.agent.core.ValuePropertyDefinition +import com.embabel.agent.rag.model.Chunk +import com.embabel.common.textio.template.JinjaProperties +import com.embabel.common.textio.template.JinjavaTemplateRenderer +import com.embabel.dice.common.SourceAnalysisContext +import com.embabel.dice.common.resolver.AlwaysCreateEntityResolver +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.core.io.DefaultResourceLoader + +/** + * `suggest_entities` renders the list of types the model is told to choose from. + * + * The regression these pin: the template read `domainType.clazz.simpleName`, and `clazz` exists + * only on `JvmType`. Every [DynamicType] — the schema-declared types, which are most of them in a + * configured world — therefore rendered with an EMPTY name under "Entity types must only come from + * the following list", while Jinjava logged one "Cannot resolve property 'clazz'" per type per + * chunk. A prompt that names none of its types cannot be obeyed. + */ +class SuggestEntitiesTemplateTest { + + private lateinit var renderer: JinjavaTemplateRenderer + + @BeforeEach + fun setUp() { + renderer = JinjavaTemplateRenderer( + jinja = JinjaProperties(prefix = "classpath:/prompts/", suffix = ".jinja"), + resourceLoader = DefaultResourceLoader(), + ) + } + + private val meeting = DynamicType( + name = "calendar.meeting", + description = "A calendar event surfaced as a signal", + ownProperties = listOf( + ValuePropertyDefinition( + name = "subject", + type = "string", + cardinality = Cardinality.ONE, + description = "Event title", + ), + ), + ) + + private fun render(extra: Map = emptyMap()): String { + val context = SourceAnalysisContext( + schema = DataDictionary.fromDomainTypes("test", listOf(meeting)), + entityResolver = AlwaysCreateEntityResolver, + contextId = ContextId("test-context"), + ) + return renderer.renderLoadedTemplate( + "suggest_entities", + mapOf( + "context" to context, + "chunk" to Chunk.create(text = "Test chunk text", parentId = "source-1"), + ) + extra, + ) + } + + @Test + fun `a dynamic type is named in the list of permitted types`() { + val result = render() + assertTrue(result.contains("Meeting"), "the type's label is rendered, in:\n$result") + assertTrue(result.contains("A calendar event surfaced as a signal"), "its description too") + assertTrue(result.contains("subject"), "and its properties") + } + + @Test + fun `directions render when a caller supplies them and are silent otherwise`() { + assertFalse( + render().contains("Consider the following direction"), + "no directions supplied — the block stays out of the prompt", + ) + assertTrue( + render(mapOf("directions" to "focus on scheduling")) + .contains("Consider the following direction: focus on scheduling"), + ) + } +} diff --git a/specs/dice-and-coala.md b/specs/dice-and-coala.md new file mode 100644 index 00000000..398e413e --- /dev/null +++ b/specs/dice-and-coala.md @@ -0,0 +1,102 @@ +# DICE and CoALA + +How DICE relates to the CoALA cognitive-architecture framework, and how to use DICE as the +long-term memory engine inside a CoALA-based agent architecture. + +## The short answer + +"Why GUM instead of CoALA?" is a category error. The two operate at different levels: + +- **CoALA** ([Sumers, Yao, Narasimhan & Griffiths, arXiv:2309.02427](https://arxiv.org/abs/2309.02427), + TMLR 2024) is a **cognitive-architecture framework**: a taxonomy that organizes a language agent + around working memory, three kinds of long-term memory (episodic, semantic, procedural), an action + space (internal reasoning/retrieval/learning actions, external grounding actions), and a + decision-making loop. It tells you *what subsystems an agent should have*. It deliberately does + not specify mechanisms — how propositions are extracted, deduplicated, revised, decayed, + consolidated, or audited. +- **GUM** ([Shaikh et al., arXiv:2505.10831](https://arxiv.org/abs/2505.10831), Stanford/Microsoft + 2025) is a **concrete, empirically validated mechanism** for one of those subsystems: building a + semantic user model from confidence-weighted propositions, with published accuracy results + (76% overall, 100% for high-confidence propositions). + +DICE did not choose GUM *over* CoALA. It chose GUM as the implementation mechanism for the +long-term memory component that a CoALA-style architecture calls for. An organization +standardizing on CoALA as its architectural vocabulary still needs a concrete engine behind each +memory box in the diagram. DICE is that engine for long-term memory — designed for the JVM, +integrated with existing enterprise entities, and auditable. + +## Mapping DICE onto CoALA's memory taxonomy + +DICE classifies every proposition with a `KnowledgeType` that corresponds one-to-one with CoALA's +memory taxonomy: + +| CoALA memory | DICE `KnowledgeType` | DICE mechanism | +|---|---|---| +| **Semantic** — facts about the world and the user | `SEMANTIC` | The core proposition store: confidence-weighted, entity-resolved, revised through five-way classification, decaying over time. Low decay, long-lived. | +| **Episodic** — records of the agent's experiences | `EPISODIC` | Propositions with temporal context and higher decay, plus the **grounding chain**: every proposition links back to the source chunks it was extracted from, so the raw experience record is retained alongside the distilled knowledge. | +| **Procedural** — how things are done | `PROCEDURAL` | Declarative statements of preference, habit, and rule ("prefers X", "when deploying, use Y") that steer agent behaviour. Executable skills and code remain the agent framework's concern (see below). | +| **Working** — current, session-scoped context | `WORKING` | Transient propositions not yet consolidated into long-term memory. Working-memory *management* — the context assembled for each LLM call — belongs to the agent framework, with DICE's memory projection supplying the long-term contribution to it. | + +Two CoALA internal action types also have direct DICE counterparts: + +- **Retrieval actions** — CoALA's "read from long-term memory into working memory" — are DICE's + projections and query surfaces: vector similarity, canonical match, entity-based lookup, + composable `PropositionQuery`, graph traversal via the Neo4j projection, Prolog inference, + and natural-language QA via the Oracle. +- **Learning actions** — CoALA's "write experience into long-term memory" — are the proposition + pipeline (extract → resolve → revise) plus the consolidation dream loop, which synthesizes + higher-level propositions from lower-level ones (episodic → semantic distillation, with source + tracking through the abstraction hierarchy). + +## What CoALA leaves open — and DICE's answers + +CoALA is explicit that it is a conceptual framework, not an implementation. Building a real +memory system from it means making mechanism decisions the paper does not make. These are exactly +the decisions DICE takes a position on: + +| Open question in a CoALA build | DICE's answer | +|---|---| +| What is a memory unit? | A structured **proposition**: text, typed entity mentions, confidence, decay, grounding, reinforcement count, abstraction level — not a flat fact string. | +| How is new information reconciled with old? | Five-way revision classification (IDENTICAL / SIMILAR / CONTRADICTORY / UNRELATED / GENERALIZES) with outcome-dependent confidence adjustment, batched for throughput, with deterministic fast paths that skip the LLM. | +| What happens on contradiction? | **Both propositions are retained** with reduced confidence — history is never silently deleted. | +| How does memory age? | Effective confidence = `confidence * exp(-decay * k * age_days)` (the GUM formula), so transient knowledge fades and stable knowledge persists, without hard deletion. | +| How does memory connect to existing systems? | A seven-strategy entity-resolution pipeline links mentions to the organization's *existing* entities (any `NamedEntityDataRepository`), rather than creating a parallel entity universe. | +| How is consolidation performed? | Admission gates, mark-and-sweep reclamation with an audit trail, and the consolidation dream loop producing multi-level abstractions with source lineage. | +| How is memory trusted and audited? | Per-proposition provenance (grounding chain to source chunks), source authority and trust scoring, reinforcement counting, and query-time authority filtering. | + +## Using DICE inside a CoALA-based architecture + +For an organization adopting CoALA as its organizing framework: + +1. **Keep CoALA as the architectural vocabulary.** It is a good shared language for what an agent + needs: the memory taxonomy, the action space, the decision loop. +2. **Let the agent framework own the decision loop and working memory.** CoALA locates planning, + action selection, and working-memory management in the agent itself. In the Embabel stack that + is the [Embabel Agent Framework](https://github.com/embabel/embabel-agent) (typed actions, + goal-oriented planning, blackboard state); DICE deliberately does not duplicate it. +3. **Use DICE as the long-term memory engine** behind the semantic, episodic, and (declarative) + procedural boxes, with the memory projection feeding the long-term contribution to working + context. +4. **Exploit the audit properties in regulated environments.** Because contradictions are retained + rather than deleted, every proposition is grounded in source evidence, and confidence evolution + is explicit, the question "why does the system believe X, and how sure is it?" has an + inspectable answer. Memory systems that hard-delete on conflict cannot reconstruct that history. +5. **Deploy inside the perimeter.** DICE is an embeddable JVM library with no mandatory cloud + service or graph-database dependency — relevant where data residency and vendor-risk review + constrain managed memory services. + +## What DICE does not attempt + +Honesty about scope, in CoALA's terms: + +- **The decision loop.** DICE is memory, not an agent. Planning, action selection, and grounding + actions belong to the agent framework. +- **Working-memory management.** DICE classifies and stores working-type knowledge and supplies + retrieval into context, but assembling each LLM call's context window is the caller's job. +- **Procedural memory as executable skills.** CoALA's procedural memory includes the agent's own + code and learned skills. DICE covers the declarative slice (preferences, habits, rules as + propositions); skill acquisition and storage are out of scope. + +For how DICE compares against other *memory implementations* (Zep/Graphiti, Mem0, LangMem, the +Google/AWS/Microsoft managed offerings, Neo4j Agent Memory, LiveGraph), see +[competitive-positioning.md](competitive-positioning.md).