diff --git a/apps/entities/agents/entities.txt b/apps/entities/agents/entities.txt index 26346cd4a..dc0d41b82 100644 --- a/apps/entities/agents/entities.txt +++ b/apps/entities/agents/entities.txt @@ -1,6 +1,6 @@ ## Core Mission -Mine the journal for entity mentions (People, Organizations, Projects, Tools, and other relevant entities) within this specific facet's journal content and record them as facet-scoped detected entities with day-specific context. Record ALL entities encountered in this facet yesterday, even if already attached to this facet, to maintain a complete history of daily entity interactions within this facet. +Mine the journal for entity mentions (People, Companies, Projects, Tools, and other relevant entities) within this specific facet's journal content and record them as facet-scoped detected entities with day-specific context. Record ALL entities encountered in this facet yesterday, even if already attached to this facet, to maintain a complete history of daily entity interactions within this facet. ## ⚠️ CRITICAL FACET SCOPING RULE @@ -95,12 +95,12 @@ For each entity candidate: - These are the most valuable entities for context - Type: Person -2. **Medium Priority - Organizations/Companies and Projects** (selective): - - Organizations/Companies: Record only significant business relationships (clients, vendors, partners actively discussed) +2. **Medium Priority - Companies and Projects** (selective): + - Companies: Record only significant business relationships (clients, vendors, partners actively discussed) - Projects: Record only when clearly central to the discussion (actively worked on, planned, or reviewed) - Skip: passing mentions, tangential references - Ask: "Is this relationship/project important to track?" - - Types: Organization, Project, Location, Event, or other appropriate descriptors + - Types: Company, Project, or other appropriate descriptors 3. **Low Priority - Tools and Resources** (rare, only when actively discussed): - Record ONLY when the subject of discussion/evaluation @@ -110,7 +110,7 @@ For each entity candidate: - Type: Tool, or other appropriate resource descriptor **Type Assignment:** -Derive the appropriate entity type from context. Common types include Person, Organization, Project, Tool, Location, Event. Use the most specific and accurate type that describes the entity. +Derive the appropriate entity type from context. Common types include Person, Company, Project, Tool. Use the most specific and accurate type that describes the entity. **Day-Specific Description:** - Capture HOW the entity appeared yesterday (NOT generic bio) diff --git a/apps/entities/agents/entities_review.txt b/apps/entities/agents/entities_review.txt index c22e747e4..bbf895b27 100644 --- a/apps/entities/agents/entities_review.txt +++ b/apps/entities/agents/entities_review.txt @@ -55,11 +55,11 @@ Auto-promote entities based on **type-specific thresholds**: - Even 2 appearances indicates ongoing relevance - Type: Person -2. **Medium Priority - Organizations and Projects** (selective): - - Organizations: Require 3+ detections in last 7 days +2. **Medium Priority - Companies and Projects** (selective): + - Companies: Require 3+ detections in last 7 days - Projects: Require 3-4+ detections in last 7 days - Rationale: Only important business relationships and central projects warrant promotion - - Types: Organization, Project, Location, Event, or other appropriate descriptors + - Types: Company, Project, or other appropriate descriptors 3. **Low Priority - Tools and Resources** (very rare): - Require: 5+ detections in last 7 days @@ -70,8 +70,8 @@ Auto-promote entities based on **type-specific thresholds**: **Universal Requirements (all types):** **Type Consistency**: Same entity type across all detections -- All detections agree on the entity type (e.g., Person, Organization, Project, Tool) -- No ambiguity (e.g., "Apple" as both Organization and Project) +- All detections agree on the entity type (e.g., Person, Company, Project, Tool) +- No ambiguity (e.g., "Apple" as both Company and Project) **Not Already Attached to THIS Facet**: Entity name not in `entity_list(facet)` results - Avoid duplicates within this facet @@ -161,7 +161,7 @@ If multiple variations of same person detected: - Promote with full name, count all variations toward threshold - Add nickname in Phase 5 using entity_add_aka -**Organization Abbreviations:** +**Company Abbreviations:** If both full name and abbreviation detected: - "Federal Aviation Administration" (2x) and "FAA" (4x) both detected (6 total) - Promote with full name, count all variations toward threshold @@ -171,9 +171,9 @@ If both full name and abbreviation detected: ### DO: - Review full 7-day window systematically -- Apply priority-based thresholds (People: 2+, Organizations/Projects: 3-4+, Tools: 5+) +- Apply priority-based thresholds (People: 2+, Companies/Projects: 3-4+, Tools: 5+) - Prioritize person promotions (lowest threshold) -- Be selective with organizations and conservative with projects +- Be selective with companies and conservative with projects - Be very strict with tool/resource promotions - Synthesize descriptions from multiple contexts - Remove day-specific temporal references @@ -196,7 +196,7 @@ When invoked: 3. Load detected entities for THIS facet from last 7 days 4. Aggregate by entity name (within this facet), count occurrences 5. Filter by priority-based promotion criteria: - - People: 2+, Organizations/Projects: 3-4+, Tools: 5+ + - People: 2+, Companies/Projects: 3-4+, Tools: 5+ - Type consistent, not already attached to THIS facet 6. Synthesize timeless descriptions for qualifying entities 7. Execute `entity_attach()` for each promotion to THIS facet @@ -209,9 +209,9 @@ When invoked: **All Already Attached**: If all qualifying entities are already attached to THIS facet, report "All recurring entities already attached to [facet]" -**Type Conflicts**: If entity name appears with different types within THIS facet's detections (e.g., "Mercury" as Organization and Project), skip and report the ambiguity for manual review +**Type Conflicts**: If entity name appears with different types within THIS facet's detections (e.g., "Mercury" as Company and Project), skip and report the ambiguity for manual review **Below Threshold**: Report entities close to promotion separately: -- "3 entities near promotion for [facet]: Alice (Person, 1 detection - needs 1 more), Acme Corp (Organization, 2 detections - needs 1 more)" +- "3 entities near promotion for [facet]: Alice (Person, 1 detection - needs 1 more), Acme Corp (Company, 2 detections - needs 1 more)" Remember: Promotion is a facet-specific one-way operation. Only promote entities with clear evidence of consistent relevance to THIS facet and unambiguous identity. Apply strict priority-based thresholds to maintain quality within this facet. diff --git a/apps/entities/agents/entity_assist.txt b/apps/entities/agents/entity_assist.txt index 6b5eb86a6..720cdff9a 100644 --- a/apps/entities/agents/entity_assist.txt +++ b/apps/entities/agents/entity_assist.txt @@ -39,11 +39,9 @@ Extract from user input: **Type Detection:** Use context clues to derive the appropriate type: - Personal names/titles (Dr., Ms., etc.) → **Person** -- Organizations/businesses (Inc, Corp, LLC, PBC) → **Organization** +- Organizations/businesses (Inc, Corp, LLC, PBC) → **Company** - Initiatives/codebases → **Project** - Software/frameworks/libraries → **Tool** -- Places/venues → **Location** -- Scheduled activities → **Event** - **If unclear**: Make best inference from context (default to Person for individuals) ### Step 2: Check Duplicates @@ -58,7 +56,7 @@ If entity name already exists, consider if anything in the request implies the n Execute a few targeted searches based on type: - **Person**: `search_journal("{name}", limit=3)` or `search_journal("{name}", topic="event", limit=3)` -- **Organization/Company**: `search_journal("{name}", topic="news", limit=3)` or `search_journal("{name}", limit=3)` +- **Company**: `search_journal("{name}", topic="news", limit=3)` or `search_journal("{name}", limit=3)` - **Project**: `search_journal("{name}", limit=3)` - **Tool**: `search_journal("{name}", limit=3)` @@ -84,7 +82,7 @@ Synthesize a concise, timeless description relevant to the facet: - "friend from college, works in AI safety" - "project manager for mobile initiatives" -**Organization/Company format:** +**Company format:** - Industry + relationship (under 80 chars) - Examples: - "AI research company, creator of Claude" diff --git a/apps/entities/agents/entity_describe.txt b/apps/entities/agents/entity_describe.txt index 3af1337a5..2ff85450c 100644 --- a/apps/entities/agents/entity_describe.txt +++ b/apps/entities/agents/entity_describe.txt @@ -8,7 +8,7 @@ Generate a clear, informative single-sentence description for an attached entity ## Input Context You receive: -1. **Entity Type** - the type of entity (Person, Organization, Project, Tool, etc.) +1. **Entity Type** - the type of entity (Person, Company, Project, Tool, etc.) 2. **Entity Name** - the name to describe 3. **Facet** - the facet this entity belongs to (provides context for relevance) 4. **Current Description** - existing description if any (may be empty) @@ -38,7 +38,7 @@ Use these MCP tools for quick research (be efficient, 2-3 calls max): - "Senior backend engineer leading the API migration project" - "Friend from college, works in climate tech" -- **Organization**: Industry + relationship +- **Company**: Industry + relationship - "AI research company, creator of Claude" - "Healthcare consulting client since Q3 2024" diff --git a/apps/entities/routes.py b/apps/entities/routes.py index 041d3d956..de6ac6808 100644 --- a/apps/entities/routes.py +++ b/apps/entities/routes.py @@ -744,6 +744,14 @@ def get_journal_entities_data() -> dict: return {"entities": entities} +@entities_bp.route("/api/types") +def get_entity_types() -> Any: + """Return the standard entity types for UI suggestions.""" + from think.entities import ENTITY_TYPES + + return jsonify({"types": ENTITY_TYPES}) + + @entities_bp.route("/api/journal") def get_journal_entities() -> Any: """Get all journal entities with facet relationship summaries.""" diff --git a/apps/entities/tools.py b/apps/entities/tools.py index 8ee75e30e..6de7480ce 100644 --- a/apps/entities/tools.py +++ b/apps/entities/tools.py @@ -143,7 +143,7 @@ def entity_detect( Args: day: Day in YYYYMMDD format when entity was detected facet: Facet name (e.g., "personal", "work") - type: Entity type (e.g., Person, Organization, Project, Tool, Location, Event) + type: Entity type (e.g., Person, Company, Project, Tool) name: Entity name (e.g., "John Smith", "Acme Corp") description: Day-specific description of the entity @@ -223,7 +223,7 @@ def entity_attach( Args: facet: Facet name (e.g., "personal", "work") - type: Entity type (e.g., Person, Organization, Project, Tool, Location, Event) + type: Entity type (e.g., Person, Company, Project, Tool) name: Entity name (e.g., "John Smith", "Acme Corp") - must be unique in facet description: Persistent description of the entity @@ -470,7 +470,9 @@ def entity_add_aka( entities = load_entities(facet, day=None, include_detached=True) # Check if aka conflicts with another entity's name or aka - conflict = validate_aka_uniqueness(aka, entities, exclude_entity_name=resolved_name) + conflict = validate_aka_uniqueness( + aka, entities, exclude_entity_name=resolved_name + ) if conflict: return { "error": f"Alias '{aka}' conflicts with existing entity '{conflict}'", diff --git a/apps/entities/workspace.html b/apps/entities/workspace.html index 70dcd08c1..d6720661d 100644 --- a/apps/entities/workspace.html +++ b/apps/entities/workspace.html @@ -837,10 +837,7 @@ tr:hover .entity-delete-btn {
@@ -950,8 +947,51 @@ let currentDetailEntity = null; const pendingEntities = new Map(); // agent_id → { name, element } const pendingAgentCallbacks = new Map(); // agent_id → callback function -// Type display order for grouping entity cards -const TYPE_ORDER = ['Person', 'Company', 'Project', 'Tool']; +// Standard entity types - fetched from server +let ENTITY_TYPES = []; + +// Load entity types from server and populate UI elements +async function loadEntityTypes() { + try { + const resp = await fetch('/app/entities/api/types'); + const data = await resp.json(); + ENTITY_TYPES = data.types || []; + + // Populate edit dropdown + const select = document.getElementById('journal-detail-edit-type'); + if (select) { + select.innerHTML = ''; + ENTITY_TYPES.forEach(t => { + const opt = document.createElement('option'); + opt.value = t.name; + opt.textContent = t.name; + select.appendChild(opt); + }); + } + } catch (e) { + console.error('Failed to load entity types:', e); + // Fallback to defaults if fetch fails + ENTITY_TYPES = [ + {name: 'Person'}, {name: 'Company'}, {name: 'Project'}, {name: 'Tool'} + ]; + // Populate dropdown with fallback types + const select = document.getElementById('journal-detail-edit-type'); + if (select) { + select.innerHTML = ''; + ENTITY_TYPES.forEach(t => { + const opt = document.createElement('option'); + opt.value = t.name; + opt.textContent = t.name; + select.appendChild(opt); + }); + } + } +} + +// Get type ordering for display (standard types first, then custom) +function getTypeOrder() { + return ENTITY_TYPES.map(t => t.name); +} // Parse YYYYMMDD string to Date object function parseYYYYMMDD(dateStr) { @@ -1628,6 +1668,7 @@ function reattachEntity(entity) { // Edit form submission document.addEventListener('DOMContentLoaded', function() { + loadEntityTypes(); // Load entity types first for UI elements loadEntities(); setupCortexListener(); @@ -1767,8 +1808,9 @@ function renderJournalEntities() { groups[type].push(entity); }); - // Render groups in order - const orderedTypes = [...TYPE_ORDER, ...Object.keys(groups).filter(t => !TYPE_ORDER.includes(t))]; + // Render groups in order (standard types first, then custom) + const typeOrder = getTypeOrder(); + const orderedTypes = [...typeOrder, ...Object.keys(groups).filter(t => !typeOrder.includes(t))]; orderedTypes.forEach(type => { const typeEntities = groups[type]; @@ -1782,7 +1824,7 @@ function renderJournalEntities() { const header = document.createElement('div'); header.className = 'entity-type-header'; - header.textContent = pluralizeType(type); + header.textContent = type; groupDiv.appendChild(header); const grid = document.createElement('div'); @@ -1887,8 +1929,9 @@ function renderEntityCards() { groups[type].push(entity); }); - // Render groups in order - const orderedTypes = [...TYPE_ORDER, ...Object.keys(groups).filter(t => !TYPE_ORDER.includes(t))]; + // Render groups in order (standard types first, then custom) + const typeOrder = getTypeOrder(); + const orderedTypes = [...typeOrder, ...Object.keys(groups).filter(t => !typeOrder.includes(t))]; orderedTypes.forEach(type => { const entities = groups[type]; @@ -1902,7 +1945,7 @@ function renderEntityCards() { const header = document.createElement('div'); header.className = 'entity-type-header'; - header.textContent = pluralizeType(type); + header.textContent = type; groupDiv.appendChild(header); const grid = document.createElement('div'); @@ -2360,11 +2403,4 @@ function confirmEntityDelete() { loadEntities(); }); } - -// Utility -function pluralizeType(type) { - if (type === 'Person') return 'People'; - if (type === 'Company') return 'Companies'; - return type + 's'; -} diff --git a/tests/test_entity_validation.py b/tests/test_entity_validation.py index 8445bddf3..87a25edd2 100644 --- a/tests/test_entity_validation.py +++ b/tests/test_entity_validation.py @@ -3,13 +3,20 @@ """Tests for entity type validation.""" -import pytest +from think.entities import ENTITY_TYPES, is_valid_entity_type -from think.entities import is_valid_entity_type +def test_entity_types_constant(): + """Test that ENTITY_TYPES constant has expected structure.""" + assert len(ENTITY_TYPES) == 4 + assert all("name" in t for t in ENTITY_TYPES) + # Verify standard types in expected order + names = [t["name"] for t in ENTITY_TYPES] + assert names == ["Person", "Company", "Project", "Tool"] -def test_valid_original_types(): - """Test that original 4 entity types are still valid.""" + +def test_valid_standard_types(): + """Test that standard 4 entity types are valid.""" assert is_valid_entity_type("Person") is True assert is_valid_entity_type("Company") is True assert is_valid_entity_type("Project") is True diff --git a/think/entities.py b/think/entities.py index eba0448f0..baf33f7f9 100644 --- a/think/entities.py +++ b/think/entities.py @@ -31,6 +31,15 @@ from think.utils import get_config, get_journal # Used as fallback in entity_last_active_ts() to ensure all entities have a sortable value DEFAULT_ACTIVITY_TS = 1767225600000 +# Standard entity types - used for UI suggestions and documentation. +# Custom types are still allowed (validated by is_valid_entity_type regex). +ENTITY_TYPES = [ + {"name": "Person"}, + {"name": "Company"}, + {"name": "Project"}, + {"name": "Tool"}, +] + def get_identity_names() -> list[str]: """Get all names/aliases for the journal principal from identity config.