Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions hindsight-api-slim/hindsight_api/banner.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,6 @@ def color_end(text: str) -> str:
return color(text, 1.0)


def color_mid(text: str) -> str:
"""Color text with gradient middle color."""
return color(text, 0.5)


def dim(text: str) -> str:
"""Dim/gray text."""
return f"\033[38;2;128;128;128m{text}\033[0m"
Expand Down
34 changes: 4 additions & 30 deletions hindsight-api-slim/hindsight_api/daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,7 @@
import sys
import time
from pathlib import Path
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from typing import IO
from typing import IO

logger = logging.getLogger(__name__)

Expand All @@ -42,37 +39,28 @@ def __init__(self, app, idle_timeout: int = DEFAULT_IDLE_TIMEOUT):
self.app = app
self.idle_timeout = idle_timeout
self.last_activity = time.time()
self._checker_task = None

async def __call__(self, scope, receive, send):
# Update activity timestamp on each request
self.last_activity = time.time()
await self.app(scope, receive, send)

def start_idle_checker(self):
"""Start the background task that checks for idle timeout."""
self._checker_task = asyncio.create_task(self._check_idle())

async def _check_idle(self):
"""Background task that exits the process after idle timeout."""
# If idle_timeout is 0, don't auto-exit
"""Exit the daemon after the configured period without requests."""
if self.idle_timeout <= 0:
return

while True:
await asyncio.sleep(30) # Check every 30 seconds
await asyncio.sleep(30)
idle_time = time.time() - self.last_activity
if idle_time > self.idle_timeout:
logger.info(f"Idle timeout reached ({self.idle_timeout}s), shutting down daemon")
# Give a moment for any in-flight requests
await asyncio.sleep(1)
# Send SIGTERM to ourselves to trigger graceful shutdown
import signal

os.kill(os.getpid(), signal.SIGTERM)


def _detach_popen_kwargs(log_handle: "IO[bytes]") -> dict:
def _detach_popen_kwargs(log_handle: IO[bytes]) -> dict:
"""Cross-platform kwargs to spawn a subprocess detached from the caller.

On POSIX, ``start_new_session=True`` calls ``setsid(2)`` so the child
Expand Down Expand Up @@ -169,17 +157,3 @@ def daemonize():
subprocess.Popen(cmd, env=env, **_detach_popen_kwargs(log_handle))

sys.exit(0)


def check_daemon_running(port: int = DEFAULT_DAEMON_PORT) -> bool:
"""Check if a daemon is running and responsive on the given port."""
import socket

try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(1)
result = sock.connect_ex(("127.0.0.1", port))
sock.close()
return result == 0
except Exception:
return False
2 changes: 1 addition & 1 deletion hindsight-api-slim/hindsight_api/engine/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

This package contains all the implementation details of the memory engine:
- MemoryEngine: Main class for memory operations
- Utility modules: embedding_utils, link_utils, think_utils, bank_utils
- Utility modules: embedding_utils, link_utils, bank_utils
- Supporting modules: embeddings, cross_encoder, entity_resolver, etc.
"""

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2030,30 +2030,6 @@ async def _execute_delete_action(
logger.debug(f"Deleted observation {observation_id}")


async def _create_memory_links(
conn: "Connection",
memory_id: uuid.UUID,
observation_id: uuid.UUID,
) -> None:
"""
Placeholder for observation link creation.

Observations do NOT get any memory_links copied from their source facts.
Instead, retrieval uses source_memory_ids to traverse:
- Entity connections: observation → source_memory_ids → unit_entities
- Semantic similarity: observations have their own embeddings
- Temporal proximity: observations have their own temporal fields

This avoids data duplication and ensures observations are always
connected via their source facts' relationships.

The memory_id and observation_id parameters are kept for interface
compatibility but no links are created.
"""
# No links are created - observations rely on source_memory_ids for traversal
pass


async def _find_related_observations(
memory_engine: "MemoryEngine",
bank_id: str,
Expand Down
57 changes: 2 additions & 55 deletions hindsight-api-slim/hindsight_api/engine/consolidation/prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,8 @@

9. KEEP DISTINCT TOPICS DISTINCT: do not merge observations about different people, entities, or unrelated topics. Merging is for the same canonical fact recurring — not for related-but-distinct claims."""

# Field-by-field definitions of the input shape, shared by the cached system
# prefix (_INPUT_FORMAT_NOTE) and the single-message prompt (_INPUT_SECTION) so
# the two descriptions cannot drift apart. Both call sites run .format(), so
# these strings must contain no braces.
# Field-by-field definitions of the input shape used by the cached system
# prefix. The call site runs .format(), so these strings must contain no braces.
_FACT_FIELDS = """One per line, formatted as `[uuid] fact text (temporal fields)`:
- `[uuid]`: the fact's identifier — copy it verbatim into `source_fact_ids`
- `occurred_start` / `occurred_end`: when the described event happened. This can be long before the fact was stated — a fact recorded today may describe a 2019 event.
Expand Down Expand Up @@ -83,24 +81,6 @@

{observations_text}"""

# Data section — format placeholders {facts_text} and {observations_text} are substituted at call time
_INPUT_SECTION = f"""## INPUT

Every temporal field below is optional and is omitted when unknown.

### New facts

{_FACT_FIELDS}

{{facts_text}}

### Existing observations

JSON array, pooled from recalls across all new facts above. Each entry has:
{_OBSERVATION_FIELDS}

{{observations_text}}"""

_DECISION_GUIDE = """## DECISION GUIDE

- **Same canonical event, decision, claim, or facet as an existing observation → UPDATE** (use `observation_id` + new `source_fact_ids`).
Expand Down Expand Up @@ -161,39 +141,6 @@
- Return `{{"creates": [], "updates": [], "deletes": []}}` if nothing durable is found."""


def build_batch_consolidation_prompt(
observations_mission: str | None = None,
observation_capacity_note: str | None = None,
llm_output_language: str | None = None,
) -> str:
"""
Build the consolidation prompt for batch mode (multiple facts per LLM call).

The mission defines *what* to track (customisable per bank) and takes
priority over the built-in processing rules when the two conflict.
Processing rules, decision guide, and output format are always present.
When ``llm_output_language`` is set, observations are emitted in that
language.
"""
mission = escape_for_prompt(observations_mission or _DEFAULT_MISSION)

capacity_section = ""
if observation_capacity_note:
capacity_section = f"\n\n## CAPACITY CONSTRAINT\n\n{escape_for_prompt(observation_capacity_note)}"

return (
"You are a memory consolidation system. Synthesize new facts into "
"observations, merging with existing observations when appropriate.\n\n"
f"## MISSION\n\n{mission}\n\n"
f"{_MISSION_PRIORITY_NOTE}"
f"{capacity_section}\n\n"
f"{_PROCESSING_RULES}\n\n"
f"{_INPUT_SECTION}\n\n"
f"{_DECISION_GUIDE}\n\n"
f"{_OUTPUT_SECTION}" + output_language_directive(llm_output_language)
)


def build_consolidation_system_prompt(
llm_output_language: str | None = None,
) -> str:
Expand Down
3 changes: 0 additions & 3 deletions hindsight-api-slim/hindsight_api/engine/db/oracle.py
Original file line number Diff line number Diff line change
Expand Up @@ -447,9 +447,6 @@ def _rewrite_interval(m):
if has_for_update:
# FOR UPDATE path: use ROWNUM instead of FETCH FIRST.
# Extract and remove LIMIT clause, inject ROWNUM into WHERE.
def _limit_to_rownum(m):
return "" # Remove the LIMIT clause; we'll add ROWNUM below

limit_val = None
limit_match = re.search(r"\bLIMIT\s+(\d+|:\w+)\b", query, re.IGNORECASE)
if limit_match:
Expand Down
29 changes: 0 additions & 29 deletions hindsight-api-slim/hindsight_api/engine/db_budget.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,35 +164,6 @@ def wrap_pool(self, pool: Any) -> "BudgetedPool":
"""
return BudgetedPool(pool, self)

async def acquire_many(
self,
pool: Any,
count: int,
) -> AsyncIterator[list[Any]]:
"""
Acquire multiple connections within the budget.

Note: This acquires connections sequentially to respect the budget.
For parallel acquisition, use multiple acquire() calls with asyncio.gather().
This method is intended for use with raw asyncpg pools only, not DatabaseBackend.

Args:
pool: asyncpg connection pool (raw pool only)
count: Number of connections to acquire

Yields:
List of database connections
"""
connections = []
try:
for _ in range(count):
conn = await pool.acquire()
connections.append(conn)
yield connections
finally:
for conn in connections:
await pool.release(conn)


# Global default manager instance
_default_manager: ConnectionBudgetManager | None = None
Expand Down
55 changes: 0 additions & 55 deletions hindsight-api-slim/hindsight_api/engine/entity_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -948,58 +948,3 @@ async def _link_units_to_entities_batch_impl(self, conn, unit_entity_pairs: list
_CooccurrencePair(entity_id_1=e1, entity_id_2=e2, event_date=ed)
for (e1, e2), ed in cooccurrence_pairs.items()
)

async def get_units_by_entity(self, entity_id: str, limit: int = 100) -> list[str]:
"""
Get all units that mention an entity.

Args:
entity_id: Entity ID
limit: Max results

Returns:
List of unit IDs
"""
async with acquire_with_retry(self.pool) as conn:
rows = await conn.fetch(
f"""
SELECT unit_id
FROM {fq_table("unit_entities")}
WHERE entity_id = $1
ORDER BY unit_id
LIMIT $2
""",
entity_id,
limit,
)
return [row["unit_id"] for row in rows]

async def get_entity_by_text(
self,
bank_id: str,
entity_text: str,
) -> str | None:
"""
Find an entity by text (for query resolution).

Args:
bank_id: bank ID
entity_text: Entity text to search for

Returns:
Entity ID if found, None otherwise
"""
async with acquire_with_retry(self.pool) as conn:
row = await conn.fetchrow(
f"""
SELECT id FROM {fq_table("entities")}
WHERE bank_id = $1
AND canonical_name ILIKE $2
ORDER BY mention_count DESC
LIMIT 1
""",
bank_id,
entity_text,
)

return row["id"] if row else None
10 changes: 0 additions & 10 deletions hindsight-api-slim/hindsight_api/engine/memory_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -3299,16 +3299,6 @@ async def _get_backend(self) -> DatabaseBackend:
await self.initialize()
return self._backend

async def _acquire_connection(self):
"""
Acquire a connection from the database backend.

Yields a DatabaseConnection from the backend's connection pool.
"""
backend = await self._get_backend()
async with backend.acquire() as conn:
yield conn

async def health_check(self) -> dict:
"""
Perform a health check by querying the database.
Expand Down
Loading
Loading