improvements benchmark and perf

This commit is contained in:
Nicolò Boschi 2025-10-31 14:07:45 +01:00
parent 8c698d6dfb
commit 75f40fcc3e
40 changed files with 15420 additions and 8395 deletions

100
README.md
View file

@ -96,19 +96,78 @@ The search algorithm explores the memory graph using spreading activation:
4. **Thinking Budget**: Limit exploration to N units (controls computational cost)
5. **Dynamic Weighting**: Combine activation, semantic similarity, recency, and frequency:
```
final_weight = 0.30 × activation + 0.30 × semantic_similarity + 0.25 × recency + 0.15 × frequency
final_weight = w_a × activation + w_s × semantic_similarity + w_r × recency + w_f × frequency
# Default weights (configurable via search parameters):
w_a = 0.30 # Activation weight
w_s = 0.30 # Semantic similarity weight
w_r = 0.25 # Recency weight
w_f = 0.15 # Frequency weight
semantic_similarity = cosine_similarity(query_embedding, memory_embedding)
recency = exp(-0.1 × days_since)
recency = 1 / (1 + log(1 + days_since/365)) # Logarithmic decay with 1-year half-life
frequency = normalized to [0, 1] from log(access_count + 1) / log(10)
```
**Weight Tuning**: All weights are configurable via `search_async()` parameters, enabling benchmark experiments with different scoring strategies (e.g., emphasizing graph structure vs semantic similarity).
Recency uses logarithmic decay to provide meaningful differentiation over years:
- Today: 1.000 (100% weight)
- 1 week: 0.981 (barely any decay)
- 1 month: 0.927 (still very recent)
- 3 months: 0.819 (recent)
- 6 months: 0.714
- 1 year: 0.591 (half-life point)
- 2 years: 0.477 ✓
- 5 years: 0.358 ✓ (clearly different from 2 years!)
- 10 years: 0.294 ✓
This ensures old memories (2yr vs 5yr) have different weights, unlike exponential decay.
6. **Return Top-K**: Sort by final weight and return top results
This approach ensures:
- Semantic relevance to query is always considered (30% weight)
- Graph structure influences results through activation (30% weight)
- Recently accessed memories get boosted (25% weight - recency bias)
- Frequently accessed memories get boosted (15% weight - importance signal)
- Semantic relevance to query is always considered (default 30% weight)
- Graph structure influences results through activation (default 30% weight)
- Recently accessed memories get boosted (default 25% weight - recency bias)
- Frequently accessed memories get boosted (default 15% weight - importance signal)
### Search Tracing & Debugging
The system includes comprehensive search tracing to understand and debug the search process:
**Enable tracing**:
```python
results, trace = memory.search(
agent_id="agent_1",
query="Who works at Google?",
enable_trace=True # Returns detailed SearchTrace object
)
```
**Trace captures**:
- Every node visited with parent/child relationships
- All links explored (followed or pruned) with reasons
- Weight calculations broken down by component
- Entry points selected and their similarity scores
- Pruning decisions (already visited, activation too low, budget exhausted)
- Performance metrics for each search phase
**Export trace for visualization**:
```python
# Save trace as JSON for external visualization tools
trace_json = trace.to_json()
with open("trace.json", "w") as f:
f.write(trace_json)
```
**Use cases**:
- Understanding why certain memories were/weren't retrieved
- Debugging search behavior
- Analyzing link type effectiveness
- Performance profiling
- Building custom visualization layers
See `SEARCH_TRACE.md` for complete trace API documentation and `examples/trace_example.py` for a working demo.
### Self-Contained Memory Units
@ -291,7 +350,8 @@ memory.put(
### Search Memories
```python
results = memory.search(
# Basic search (trace disabled by default)
results, trace = memory.search(
agent_id="agent_1",
query="What does Alice do?",
thinking_budget=50, # How many units to explore
@ -300,6 +360,32 @@ results = memory.search(
for result in results:
print(f"{result['text']} (weight: {result['weight']:.3f})")
# Search with tracing for debugging
results, trace = memory.search(
agent_id="agent_1",
query="What does Alice do?",
thinking_budget=50,
top_k=10,
enable_trace=True # Returns detailed SearchTrace object
)
# Analyze trace
print(f"Nodes visited: {trace.summary.total_nodes_visited}")
print(f"Entry points: {len(trace.entry_points)}")
trace_json = trace.to_json() # Export for visualization
# Search with custom weight tuning
results, trace = memory.search(
agent_id="agent_1",
query="What does Alice do?",
thinking_budget=50,
top_k=10,
weight_activation=0.40, # Emphasize graph structure
weight_semantic=0.40, # Emphasize semantic similarity
weight_recency=0.10, # De-emphasize recency
weight_frequency=0.10 # De-emphasize frequency
)
```
## How It Works: Example

View file

@ -1,6 +1,32 @@
# Benchmarks
# Benchmark Suite
This directory contains benchmark evaluations for the Entity-Aware Memory System.
This directory contains a common benchmark framework and benchmark-specific implementations for evaluating the memory system.
## Structure
```
benchmarks/
├── common/ # Common benchmark framework
│ ├── benchmark_runner.py # Main runner with all optimizations
│ └── __init__.py
├── locomo/ # LoComo benchmark
│ ├── locomo_benchmark.py # LoComo-specific implementations
│ ├── run_benchmark.py # Runner script
│ └── locomo10.json # Dataset (place here)
└── longmemeval/ # LongMemEval benchmark
├── longmemeval_benchmark.py # LongMemEval-specific implementations
├── run_benchmark.py # Runner script
└── longmemeval_s_cleaned.json # Dataset (auto-downloaded)
```
## Common Framework
The common framework provides a unified interface with optimizations from the working LoComo implementation:
- **Batch ingestion** via `put_batch_async`
- **Parallel question processing** with rate limiting
- **Parallel LLM judging** with configurable semaphore
- **Progress tracking** with Rich
- **Comprehensive metrics** collection
## LoComo Benchmark

View file

@ -0,0 +1 @@
"""Common benchmark framework."""

View file

@ -0,0 +1,538 @@
"""
Common benchmark runner framework based on the LoComo implementation.
This module provides a unified interface for running benchmarks with the same
optimizations as the working LoComo benchmark:
- Batch ingestion for speed
- Parallel question processing with semaphores
- Parallel LLM judging with rate limiting
- Progress tracking with Rich
- Comprehensive metrics collection
"""
import json
import asyncio
from abc import ABC, abstractmethod
from datetime import datetime, timezone
from typing import List, Dict, Any, Optional, Tuple
from pathlib import Path
from rich.console import Console
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
from rich.table import Table
from rich import box
import pydantic
from memory import TemporalSemanticMemory
from openai import AsyncOpenAI
console = Console()
class BenchmarkDataset(ABC):
"""Abstract base class for benchmark datasets."""
@abstractmethod
def load(self, path: Path, max_items: Optional[int] = None) -> List[Dict[str, Any]]:
"""
Load dataset from file.
Returns:
List of dataset items
"""
pass
@abstractmethod
def get_item_id(self, item: Dict) -> str:
"""Get unique identifier for an item."""
pass
@abstractmethod
def prepare_sessions_for_ingestion(self, item: Dict) -> List[Dict[str, Any]]:
"""
Prepare conversation sessions for batch ingestion.
Returns:
List of session dicts with keys: 'content', 'context', 'event_date'
"""
pass
@abstractmethod
def get_qa_pairs(self, item: Dict) -> List[Dict[str, Any]]:
"""
Extract QA pairs from an item.
Returns:
List of QA dicts with keys: 'question', 'answer', 'category' (optional)
"""
pass
class LLMAnswerGenerator(ABC):
"""Abstract base class for LLM-based answer generation."""
@abstractmethod
async def generate_answer(
self,
question: str,
memories: List[Dict[str, Any]]
) -> Tuple[str, str]:
"""
Generate answer from retrieved memories.
Returns:
Tuple of (answer, reasoning)
"""
pass
class LLMAnswerEvaluator(ABC):
"""Abstract base class for LLM-based answer evaluation."""
@abstractmethod
async def judge_answer(
self,
question: str,
correct_answer: str,
predicted_answer: str,
semaphore: asyncio.Semaphore
) -> Tuple[bool, str]:
"""
Evaluate predicted answer against correct answer.
Args:
question: The question
correct_answer: Gold/correct answer
predicted_answer: Predicted answer
semaphore: Semaphore for rate limiting
Returns:
Tuple of (is_correct, reasoning)
"""
pass
class BenchmarkRunner:
"""
Common benchmark runner using the proven LoComo approach.
Optimizations:
- Batch ingestion (put_batch_async)
- Parallel question processing with rate limiting
- Parallel LLM judging with rate limiting
- Progress tracking
"""
def __init__(
self,
dataset: BenchmarkDataset,
answer_generator: LLMAnswerGenerator,
answer_evaluator: LLMAnswerEvaluator,
memory: Optional[TemporalSemanticMemory] = None
):
"""
Initialize benchmark runner.
Args:
dataset: Dataset implementation
answer_generator: Answer generator implementation
answer_evaluator: Answer evaluator implementation
memory: Memory system instance (creates new if None)
"""
self.dataset = dataset
self.answer_generator = answer_generator
self.answer_evaluator = answer_evaluator
self.memory = memory or TemporalSemanticMemory()
async def ingest_conversation(
self,
item: Dict[str, Any],
agent_id: str
) -> int:
"""
Ingest conversation into memory using batch ingestion.
Uses put_batch_async for maximum efficiency.
Returns:
Number of sessions ingested
"""
batch_contents = self.dataset.prepare_sessions_for_ingestion(item)
if batch_contents:
await self.memory.put_batch_async(
agent_id=agent_id,
contents=batch_contents
)
return len(batch_contents)
async def answer_question(
self,
agent_id: str,
question: str,
thinking_budget: int = 500,
top_k: int = 20,
weight_activation: float = 0.30,
weight_semantic: float = 0.30,
weight_recency: float = 0.25,
weight_frequency: float = 0.15,
) -> Tuple[str, str, List[Dict]]:
"""
Answer a question using memory retrieval.
Returns:
Tuple of (answer, reasoning, retrieved_memories)
"""
# Search memory
results, _ = await self.memory.search_async(
agent_id=agent_id,
query=question,
thinking_budget=thinking_budget,
top_k=top_k,
weight_activation=weight_activation,
weight_semantic=weight_semantic,
weight_recency=weight_recency,
weight_frequency=weight_frequency,
)
if not results:
return "I don't have enough information to answer that question.", "No relevant memories found.", []
# Generate answer using LLM
answer, reasoning = await self.answer_generator.generate_answer(question, results)
return answer, reasoning, results
async def evaluate_qa_task(
self,
agent_id: str,
qa_pairs: List[Dict],
item_id: str,
thinking_budget: int,
top_k: int,
max_questions: Optional[int] = None,
semaphore: asyncio.Semaphore = None,
weight_activation: float = 0.30,
weight_semantic: float = 0.30,
weight_recency: float = 0.25,
weight_frequency: float = 0.15,
) -> List[Dict]:
"""
Evaluate QA task with parallel question processing.
Args:
semaphore: Semaphore to limit concurrent question processing
Returns:
List of QA results
"""
# Filter out questions without answers (category 5)
qa_pairs = [pair for pair in qa_pairs if pair.get('answer')]
questions_to_eval = qa_pairs[:max_questions] if max_questions else qa_pairs
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
BarColumn(),
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
console=console
) as progress:
task = progress.add_task(
f"[cyan]Evaluating QA for {item_id} - {len(questions_to_eval)} questions",
total=len(questions_to_eval)
)
# Create tasks for all questions
async def process_question(qa):
async with semaphore:
question = qa['question']
correct_answer = qa['answer']
category = qa.get('category', 0)
# Get predicted answer, reasoning, and retrieved memories
predicted_answer, reasoning, retrieved_memories = await self.answer_question(
agent_id, question, thinking_budget, top_k,
weight_activation, weight_semantic, weight_recency, weight_frequency
)
return {
'question': question,
'correct_answer': correct_answer,
'predicted_answer': predicted_answer,
'reasoning': reasoning,
'category': category,
'retrieved_memories': retrieved_memories
}
question_tasks = [process_question(qa) for qa in questions_to_eval]
# Use as_completed to update progress as results come in
results = []
for coro in asyncio.as_completed(question_tasks):
result = await coro
results.append(result)
progress.update(task, advance=1)
return results
async def calculate_metrics(self, results: List[Dict], eval_semaphore_size: int = 8) -> Dict:
"""
Calculate evaluation metrics using parallel LLM-as-judge.
Args:
results: QA results to evaluate
eval_semaphore_size: Max concurrent LLM judge requests
Returns:
Dict with evaluation metrics
"""
total = len(results)
# Semaphore to limit concurrent requests
semaphore = asyncio.Semaphore(eval_semaphore_size)
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
BarColumn(),
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
console=console
) as progress:
task = progress.add_task(
f"[yellow]Judging answers with LLM (parallel, max {eval_semaphore_size})...",
total=total
)
# Create all judgment tasks
async def judge_single(result):
is_correct, eval_reasoning = await self.answer_evaluator.judge_answer(
result['question'],
result['correct_answer'],
result['predicted_answer'],
semaphore
)
result['is_correct'] = is_correct
result['correctness_reasoning'] = eval_reasoning
return result
judgment_tasks = [judge_single(result) for result in results]
# Process in parallel with progress updates
judged_results = []
for coro in asyncio.as_completed(judgment_tasks):
judged_result = await coro
judged_results.append(judged_result)
progress.update(task, advance=1)
# Calculate stats
correct = sum(1 for r in judged_results if r.get('is_correct', False))
category_stats = {}
for result in judged_results:
category = result.get('category', 'unknown')
if category not in category_stats:
category_stats[category] = {'correct': 0, 'total': 0}
category_stats[category]['total'] += 1
if result.get('is_correct', False):
category_stats[category]['correct'] += 1
accuracy = (correct / total * 100) if total > 0 else 0
return {
'accuracy': accuracy,
'correct': correct,
'total': total,
'category_stats': category_stats,
'detailed_results': judged_results
}
async def process_single_item(
self,
item: Dict,
agent_id: str,
i: int,
total_items: int,
thinking_budget: int,
top_k: int,
max_questions_per_item: Optional[int],
skip_ingestion: bool,
question_semaphore: asyncio.Semaphore,
eval_semaphore_size: int = 8,
weight_activation: float = 0.30,
weight_semantic: float = 0.30,
weight_recency: float = 0.25,
weight_frequency: float = 0.15,
) -> Dict:
"""
Process a single item (ingest + evaluate).
Returns:
Result dict with metrics
"""
item_id = self.dataset.get_item_id(item)
console.print(f"\n[bold blue]Item {i}/{total_items}[/bold blue] (ID: {item_id})")
if not skip_ingestion:
# Clear previous agent data only on first item
if i == 1:
console.print(" [1] Clearing previous agent data...")
await self.memory.delete_agent(agent_id)
console.print(f" [green]✓[/green] Cleared '{agent_id}' agent data")
# Ingest conversation
console.print(" [2] Ingesting conversation (batch mode)...")
num_sessions = await self.ingest_conversation(item, agent_id)
console.print(f" [green]✓[/green] Ingested {num_sessions} sessions")
else:
num_sessions = -1
# Evaluate QA
qa_pairs = self.dataset.get_qa_pairs(item)
console.print(f" [3] Evaluating {len(qa_pairs)} QA pairs (parallel)...")
qa_results = await self.evaluate_qa_task(
agent_id,
qa_pairs,
item_id,
thinking_budget,
top_k,
max_questions_per_item,
question_semaphore,
weight_activation,
weight_semantic,
weight_recency,
weight_frequency
)
# Calculate metrics
console.print(" [4] Calculating metrics...")
metrics = await self.calculate_metrics(qa_results, eval_semaphore_size)
console.print(f" [green]✓[/green] Accuracy: {metrics['accuracy']:.2f}% ({metrics['correct']}/{metrics['total']})")
return {
'item_id': item_id,
'metrics': metrics,
'num_sessions': num_sessions
}
async def run(
self,
dataset_path: Path,
agent_id: str,
max_items: Optional[int] = None,
max_questions_per_item: Optional[int] = None,
thinking_budget: int = 500,
top_k: int = 20,
skip_ingestion: bool = False,
max_concurrent_questions: int = 16,
eval_semaphore_size: int = 8,
clear_agent_per_item: bool = False,
weight_activation: float = 0.30,
weight_semantic: float = 0.30,
weight_recency: float = 0.25,
weight_frequency: float = 0.15,
) -> Dict[str, Any]:
"""
Run the full benchmark evaluation.
Args:
dataset_path: Path to dataset file
agent_id: Agent ID to use
max_items: Maximum number of items to evaluate
max_questions_per_item: Maximum questions per item
thinking_budget: Thinking budget for search
top_k: Number of memories to retrieve
skip_ingestion: Skip ingestion and use existing data
max_concurrent_questions: Max concurrent question processing
eval_semaphore_size: Max concurrent LLM judge requests
clear_agent_per_item: Clear agent data before each item (for isolation)
weight_activation: Weight for activation score in final ranking (default: 0.30)
weight_semantic: Weight for semantic similarity in final ranking (default: 0.30)
weight_recency: Weight for recency score in final ranking (default: 0.25)
weight_frequency: Weight for frequency score in final ranking (default: 0.15)
Returns:
Dict with complete benchmark results
"""
console.print(f"\n[bold cyan]Benchmark Evaluation[/bold cyan]")
console.print("=" * 80)
# Load dataset
console.print(f"\n[1] Loading dataset from {dataset_path}...")
items = self.dataset.load(dataset_path, max_items)
console.print(f" [green]✓[/green] Loaded {len(items)} items")
# Initialize memory system
console.print(f"\n[2] Initializing memory system...")
console.print(f" [green]✓[/green] Memory system initialized")
# Create semaphore for question processing
question_semaphore = asyncio.Semaphore(max_concurrent_questions)
# Process items
all_results = []
for i, item in enumerate(items, 1):
# Clear agent per item if requested (for isolation in benchmarks like LongMemEval)
if clear_agent_per_item and i > 1 and not skip_ingestion:
await self.memory.delete_agent(agent_id)
result = await self.process_single_item(
item, agent_id, i, len(items),
thinking_budget, top_k, max_questions_per_item,
skip_ingestion, question_semaphore, eval_semaphore_size,
weight_activation, weight_semantic, weight_recency, weight_frequency
)
all_results.append(result)
# Calculate overall metrics
total_correct = sum(r['metrics']['correct'] for r in all_results)
total_questions = sum(r['metrics']['total'] for r in all_results)
overall_accuracy = (total_correct / total_questions * 100) if total_questions > 0 else 0
return {
'overall_accuracy': overall_accuracy,
'total_correct': total_correct,
'total_questions': total_questions,
'num_items': len(items),
'item_results': all_results
}
def display_results(self, results: Dict[str, Any]):
"""Display benchmark results in a formatted table."""
console.print("\n[bold green]✓ Benchmark Complete![/bold green]\n")
# Display results table
table = Table(title="Benchmark Results", box=box.ROUNDED)
table.add_column("Item ID", style="cyan")
table.add_column("Sessions", justify="right", style="yellow")
table.add_column("Questions", justify="right", style="blue")
table.add_column("Correct", justify="right", style="green")
table.add_column("Accuracy", justify="right", style="magenta")
for result in results['item_results']:
metrics = result['metrics']
table.add_row(
result['item_id'],
str(result['num_sessions']),
str(metrics['total']),
str(metrics['correct']),
f"{metrics['accuracy']:.1f}%"
)
table.add_row(
"[bold]OVERALL[/bold]",
"-",
f"[bold]{results['total_questions']}[/bold]",
f"[bold]{results['total_correct']}[/bold]",
f"[bold]{results['overall_accuracy']:.1f}%[/bold]"
)
console.print(table)
def save_results(self, results: Dict[str, Any], output_path: Path):
"""Save results to JSON file."""
with open(output_path, 'w') as f:
json.dump(results, f, indent=2, default=str)
console.print(f"\n[green]✓[/green] Results saved to {output_path}")

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,258 @@
"""
LoComo-specific benchmark implementations.
Provides dataset, answer generator, and evaluator for the LoComo benchmark.
"""
import sys
from pathlib import Path
# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
import json
from datetime import datetime, timezone
from typing import List, Dict, Any, Tuple, Optional
import asyncio
import pydantic
from openai import AsyncOpenAI
import os
from dotenv import load_dotenv
load_dotenv()
# Import common framework
sys.path.insert(0, str(Path(__file__).parent.parent))
from common.benchmark_runner import BenchmarkDataset, LLMAnswerGenerator, LLMAnswerEvaluator
class LoComoDataset(BenchmarkDataset):
"""LoComo dataset implementation."""
def load(self, path: Path, max_items: Optional[int] = None) -> List[Dict[str, Any]]:
"""Load LoComo dataset from JSON file."""
with open(path, 'r') as f:
dataset = json.load(f)
if max_items:
dataset = dataset[:max_items]
return dataset
def get_item_id(self, item: Dict) -> str:
"""Get sample ID from LoComo item."""
return item['sample_id']
def prepare_sessions_for_ingestion(self, item: Dict) -> List[Dict[str, Any]]:
"""
Prepare LoComo conversation sessions for batch ingestion.
Returns:
List of session dicts with 'content', 'context', 'event_date'
"""
conv = item['conversation']
speaker_a = conv['speaker_a']
speaker_b = conv['speaker_b']
# Get all session keys sorted
session_keys = sorted([k for k in conv.keys() if k.startswith('session_') and not k.endswith('_date_time')])
batch_contents = []
for session_key in session_keys:
if session_key not in conv or not isinstance(conv[session_key], list):
continue
session_data = conv[session_key]
# Build session content from all turns
session_parts = []
for turn in session_data:
speaker = turn['speaker']
text = turn['text']
session_parts.append(f"{speaker}: {text}")
if not session_parts:
continue
# Get session date
date_key = f"{session_key}_date_time"
session_date = self._parse_date(conv.get(date_key, "1:00 pm on 1 January, 2023"))
# Add to batch
session_content = "\n".join(session_parts)
batch_contents.append({
"content": session_content,
"context": f"Conversation session between {speaker_a} and {speaker_b} (conversation {item['sample_id']} session {session_key})",
"event_date": session_date
})
return batch_contents
def get_qa_pairs(self, item: Dict) -> List[Dict[str, Any]]:
"""
Extract QA pairs from LoComo item.
Returns:
List of QA dicts with 'question', 'answer', 'category'
"""
return item['qa']
def _parse_date(self, date_string: str) -> datetime:
"""Parse LoComo date format to datetime."""
# Format: "1:56 pm on 8 May, 2023"
try:
dt = datetime.strptime(date_string, "%I:%M %p on %d %B, %Y")
return dt.replace(tzinfo=timezone.utc)
except:
return datetime.now(timezone.utc)
class QuestionAnswer(pydantic.BaseModel):
"""Answer format for LoComo questions."""
answer: str
reasoning: str
class LoComoAnswerGenerator(LLMAnswerGenerator):
"""LoComo-specific answer generator using OpenAI."""
async def generate_answer(
self,
question: str,
memories: List[Dict[str, Any]]
) -> Tuple[str, str]:
"""
Generate answer from retrieved memories using OpenAI.
Returns:
Tuple of (answer, reasoning)
"""
# Format context
context_parts = []
for i, result in enumerate(memories):
context_parts.append(f"{i}. {result['text']}")
context = "\n".join(context_parts)
# Use OpenAI to generate answer
try:
client = AsyncOpenAI()
response = await client.beta.chat.completions.parse(
model="gpt-5",
messages=[
{
"role": "system",
"content": "You are a helpful expert assistant answering questions from lme_experiment users based on the provided context."
},
{
"role": "user",
"content": f"""
# CONTEXT:
You have access to facts and entities from a conversation.
# INSTRUCTIONS:
1. Carefully analyze all provided memories
2. Pay special attention to the timestamps to determine the answer
3. If the question asks about a specific event or fact, look for direct evidence in the memories
4. If the memories contain contradictory information, prioritize the most recent memory
5. Always convert relative time references to specific dates, months, or years.
6. Be as specific as possible when talking about people, places, and events
7. Timestamps in memories represent the actual time the event occurred, not the time the event was mentioned in a message.
Clarification:
When interpreting memories, use the timestamp to determine when the described event happened, not when someone talked about the event.
Example:
Memory: (2023-03-15T16:33:00Z) I went to the vet yesterday.
Question: What day did I go to the vet?
Correct Answer: March 15, 2023
Explanation:
Even though the phrase says "yesterday," the timestamp shows the event was recorded as happening on March 15th. Therefore, the actual vet visit happened on that date, regardless of the word "yesterday" in the text.
# APPROACH (Think step by step):
1. First, examine all memories that contain information related to the question
2. Examine the timestamps and content of these memories carefully
3. Look for explicit mentions of dates, times, locations, or events that answer the question
4. If the answer requires calculation (e.g., converting relative time references), show your work
5. Formulate a precise, concise answer based solely on the evidence in the memories
6. Double-check that your answer directly addresses the question asked
7. Ensure your final answer is specific and avoids vague time references
Context:
{context}
Question: {question}
Answer:
"""
}
],
response_format=QuestionAnswer
)
answer_obj = response.choices[0].message.parsed
return answer_obj.answer, answer_obj.reasoning
except Exception as e:
return f"Error generating answer: {str(e)}", "Error occurred during answer generation."
class JudgeResponse(pydantic.BaseModel):
"""Judge response format."""
correct: bool
reasoning: str
class LoComoAnswerEvaluator(LLMAnswerEvaluator):
"""LoComo-specific answer evaluator using Groq."""
def __init__(self):
"""Initialize with Groq client."""
groq_api_key = os.getenv('GROQ_API_KEY')
if not groq_api_key:
raise ValueError("GROQ_API_KEY environment variable not set")
base_url = os.getenv('GROQ_BASE_URL', 'https://api.groq.com/openai/v1')
self.client = AsyncOpenAI(
api_key=groq_api_key,
base_url=base_url
)
async def judge_answer(
self,
question: str,
correct_answer: str,
predicted_answer: str,
semaphore: asyncio.Semaphore
) -> Tuple[bool, str]:
"""
Evaluate predicted answer using Groq LLM-as-judge.
Returns:
Tuple of (is_correct, reasoning)
"""
async with semaphore:
try:
response = await self.client.beta.chat.completions.parse(
model="openai/gpt-oss-120b",
messages=[
{
"role": "system",
"content": "You are an objective judge. Determine if the predicted answer contains the correct answer or they are the same content (with different form is fine)."
},
{
"role": "user",
"content": f"Question: {question}\nCorrect answer: {correct_answer}\nPredicted answer: {predicted_answer}\n\nAre they equivalent?"
}
],
temperature=0,
max_tokens=512,
response_format=JudgeResponse
)
judgement = response.choices[0].message.parsed
return judgement.correct, judgement.reasoning
except Exception as e:
print(f"Error judging answer: {e}")
return False, f"Error: {str(e)}"

View file

@ -0,0 +1,8 @@
# LoComo Benchmark Results
**Overall Accuracy**: 41.70% (98/235)
| Sample ID | Turns | Questions | Correct | Accuracy | Multi-hop | Single-hop | Temporal | Open-domain |
|-----------|-------|-----------|---------|----------|-----------|------------|----------|-------------|
| conv-26 | 419 | 154 | 69 | 44.81% | N/A | N/A | N/A | N/A |
| conv-30 | 369 | 81 | 29 | 35.80% | N/A | N/A | N/A | N/A |

View file

@ -2,402 +2,28 @@
LoComo Benchmark Runner for Entity-Aware Memory System
Evaluates the memory system on the LoComo (Long-term Conversational Memory) benchmark.
Uses the common benchmark framework with LoComo-specific implementations.
"""
import sys
from pathlib import Path
# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
sys.path.insert(0, str(Path(__file__).parent.parent))
import json
from datetime import datetime, timezone, timedelta
from memory import TemporalSemanticMemory
from typing import List, Dict
from openai import AsyncOpenAI
import openai
from dotenv import load_dotenv
import os
import asyncio
import pydantic
from rich.console import Console
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
from rich.table import Table
from rich import box
import argparse
from memory import TemporalSemanticMemory
from locomo_benchmark import LoComoDataset, LoComoAnswerGenerator, LoComoAnswerEvaluator
from common.benchmark_runner import BenchmarkRunner
load_dotenv()
console = Console()
def get_groq_client() -> AsyncOpenAI:
"""
Get configured async Groq client for LLM judge.
Returns:
Configured AsyncOpenAI client pointing to Groq
"""
groq_api_key = os.getenv('GROQ_API_KEY')
if not groq_api_key:
raise ValueError("GROQ_API_KEY environment variable not set")
base_url = os.getenv('GROQ_BASE_URL', 'https://api.groq.com/openai/v1')
return AsyncOpenAI(
api_key=groq_api_key,
base_url=base_url
)
def parse_date(date_string: str) -> datetime:
"""Parse LoComo date format to datetime."""
# Format: "1:56 pm on 8 May, 2023"
try:
dt = datetime.strptime(date_string, "%I:%M %p on %d %B, %Y")
return dt.replace(tzinfo=timezone.utc)
except:
return datetime.now(timezone.utc)
async def ingest_conversation(memory: TemporalSemanticMemory, conversation_data: Dict, agent_id: str):
"""
Ingest a LoComo conversation into the memory system (ASYNC version).
Ingests ALL sessions in ONE batch for maximum efficiency.
Args:
memory: Memory system instance
conversation_data: Conversation data from LoComo
agent_id: Agent ID to use
"""
conv = conversation_data['conversation']
speaker_a = conv['speaker_a']
speaker_b = conv['speaker_b']
# Get all session keys sorted
session_keys = sorted([k for k in conv.keys() if k.startswith('session_') and not k.endswith('_date_time')])
# Collect all sessions as batch items
batch_contents = []
total_turns = 0
for session_key in session_keys:
if session_key not in conv or not isinstance(conv[session_key], list):
continue
session_data = conv[session_key]
# Build session content from all turns
session_parts = []
for turn in session_data:
speaker = turn['speaker']
text = turn['text']
session_parts.append(f"{speaker}: {text}")
total_turns += 1
if not session_parts:
continue
# Get session date
date_key = f"{session_key}_date_time"
session_date = parse_date(conv.get(date_key, "1:00 pm on 1 January, 2023"))
# Add to batch
session_content = "\n".join(session_parts)
batch_contents.append({
"content": session_content,
"context": f"Conversation session between {speaker_a} and {speaker_b}",
"event_date": session_date
})
# Ingest ALL sessions in ONE batch call (MUCH faster!)
if batch_contents:
await memory.put_batch_async(
agent_id=agent_id,
contents=batch_contents
)
return total_turns
class QuestionAnswer(pydantic.BaseModel):
answer: str
reasoning: str
async def answer_question(memory: TemporalSemanticMemory, agent_id: str, question: str, thinking_budget: int = 500) -> tuple[str, str, List[Dict]]:
"""
Answer a question using the memory system (ASYNC version).
Args:
memory: Memory system instance
agent_id: Agent ID
question: Question to answer
thinking_budget: How many memory units to explore
Returns:
Tuple of (answer string, reasoning string, retrieved memories list)
"""
# Search memory
results = await memory.search_async(
agent_id=agent_id,
query=question,
thinking_budget=thinking_budget,
top_k=20 # Get more results for better context
)
if not results:
return "I don't have enough information to answer that question.", "No relevant memories found.", []
context_parts = []
for i, result in enumerate(results):
context_parts.append(f"{i}. {result['text']}")
context = "\n".join(context_parts)
# Use AsyncOpenAI to generate answer from context
try:
client = AsyncOpenAI()
response = await client.beta.chat.completions.parse(
model="gpt-5",
messages=[
{
"role": "system",
"content": "You are a helpful assistant. Answer the question based ONLY on the provided context. If the context doesn't contain the answer, say 'I don't know'. In the reasoning, explain why you choose or not choose the context items for the answer."
},
{
"role": "user",
"content": f"Context:\n{context}\n\nQuestion: {question}\n\nAnswer:"
}
],
response_format=QuestionAnswer
)
answer = response.choices[0].message.parsed
return answer.answer, answer.reasoning, results
except Exception as e:
return f"Error generating answer: {str(e)}", "Error occurred during answer generation.", results
async def evaluate_qa_task(
memory: TemporalSemanticMemory,
agent_id: str,
qa_pairs: List[Dict],
sample_id: str,
max_questions: int = None
) -> Dict:
"""
Evaluate the QA task (ASYNC version - processes questions in parallel).
Returns:
Dict with evaluation metrics
"""
questions_to_eval = qa_pairs[:max_questions] if max_questions else qa_pairs
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
BarColumn(),
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
console=console
) as progress:
task = progress.add_task(f"[cyan]Evaluating QA for sample {sample_id} (parallel)...", total=len(questions_to_eval))
# Create tasks for all questions
async def process_question(qa):
question = qa['question']
correct_answer = qa['answer']
category = qa.get('category', 0)
# Get predicted answer, reasoning, and retrieved memories
predicted_answer, reasoning, retrieved_memories = await answer_question(memory, agent_id, question)
return {
'question': question,
'correct_answer': correct_answer,
'predicted_answer': predicted_answer,
'reasoning': reasoning,
'category': category,
'retrieved_memories': retrieved_memories
}
# Process all questions in parallel
question_tasks = [process_question(qa) for qa in questions_to_eval]
# Use as_completed to update progress as results come in
results = []
for coro in asyncio.as_completed(question_tasks):
result = await coro
results.append(result)
progress.update(task, advance=1)
return results
class JudgeResponse(pydantic.BaseModel):
correct: bool
reasoning: str
async def judge_single_answer(client: AsyncOpenAI, result: Dict, semaphore: asyncio.Semaphore) -> Dict:
"""
Judge a single answer using LLM (with concurrency control).
Args:
client: Async OpenAI client (Groq)
result: Result dict with question, correct_answer, predicted_answer, category
semaphore: Semaphore to limit concurrent requests
Returns:
Updated result dict with is_correct field
"""
async with semaphore:
try:
response = await client.beta.chat.completions.parse(
model="openai/gpt-oss-120b",
messages=[
{
"role": "system",
"content":
"You are an objective judge. Determine if the predicted answer contains the correct answer or they are the same content (with different form is fine)."
},
{
"role": "user",
"content": f"Question: {result['question']}\nCorrect answer: {result['correct_answer']}\nPredicted answer: {result['predicted_answer']}\n\nAre they equivalent?"
}
],
temperature=0,
max_tokens=512,
response_format=JudgeResponse
)
judgement = response.choices[0].message.parsed
result['is_correct'] = judgement.correct
result['correctness_reasoning'] = judgement.reasoning
except Exception as e:
console.print(f"[red]Error judging answer: {e}[/red]")
result['is_correct'] = False
return result
async def calculate_metrics(results: List[Dict]) -> Dict:
"""
Calculate evaluation metrics using parallel LLM-as-judge.
Processes up to 8 judgments concurrently for speed.
"""
total = len(results)
client = get_groq_client()
# Semaphore to limit to 8 concurrent requests
semaphore = asyncio.Semaphore(8)
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
BarColumn(),
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
console=console
) as progress:
task = progress.add_task("[yellow]Judging answers with LLM (parallel, max 8)...", total=total)
# Create all judgment tasks
judgment_tasks = []
for result in results:
judgment_task = judge_single_answer(client, result, semaphore)
judgment_tasks.append(judgment_task)
# Process in parallel with progress updates
judged_results = []
for coro in asyncio.as_completed(judgment_tasks):
judged_result = await coro
judged_results.append(judged_result)
progress.update(task, advance=1)
# Calculate stats
correct = sum(1 for r in judged_results if r.get('is_correct', False))
category_stats = {}
for result in judged_results:
category = result['category']
if category not in category_stats:
category_stats[category] = {'correct': 0, 'total': 0}
category_stats[category]['total'] += 1
if result.get('is_correct', False):
category_stats[category]['correct'] += 1
accuracy = (correct / total * 100) if total > 0 else 0
return {
'accuracy': accuracy,
'correct': correct,
'total': total,
'category_stats': category_stats,
'detailed_results': judged_results
}
async def process_single_conversation(
memory: TemporalSemanticMemory,
conv_data: Dict,
i: int,
total_convs: int,
max_questions_per_conv: int,
skip_ingestion: bool
) -> Dict:
"""
Process a single conversation (ingest + evaluate).
Args:
memory: Memory system instance
conv_data: Conversation data
i: Conversation index (1-based)
total_convs: Total number of conversations
max_questions_per_conv: Max questions to evaluate per conversation
skip_ingestion: Whether to skip ingestion
Returns:
Result dict with sample_id, metrics, total_turns
"""
sample_id = conv_data['sample_id']
agent_id = "locomo" # Single agent for all Locomo benchmark data
console.print(f"\n[bold blue]Conversation {i}/{total_convs}[/bold blue] (Sample ID: {sample_id})")
if not skip_ingestion:
# Clear previous locomo agent data only (multi-tenant safe)
if i == 1: # Only cleanup on first conversation
console.print(" [2] Clearing previous 'locomo' agent data...")
memory.delete_agent(agent_id)
console.print(f" [green]✓[/green] Cleared 'locomo' agent data")
# Ingest conversation (sessions processed in parallel)
console.print(" [3] Ingesting conversation (sessions in parallel)...")
total_turns = await ingest_conversation(memory, conv_data, agent_id)
console.print(f" [green]✓[/green] Ingested {total_turns} turns across multiple sessions")
else:
total_turns = -1
# Evaluate QA (async - questions processed in parallel)
console.print(f" [4] Evaluating {len(conv_data['qa'])} QA pairs (parallel)...")
qa_results = await evaluate_qa_task(
memory,
agent_id,
conv_data['qa'],
sample_id,
max_questions=max_questions_per_conv
)
# Calculate metrics (async with parallel LLM judging)
console.print(" [5] Calculating metrics...")
metrics = await calculate_metrics(qa_results)
console.print(f" [green]✓[/green] Accuracy: {metrics['accuracy']:.2f}% ({metrics['correct']}/{metrics['total']})")
return {
'sample_id': sample_id,
'metrics': metrics,
'total_turns': total_turns
}
def run_benchmark(max_conversations: int = None, max_questions_per_conv: int = None, skip_ingestion: bool = False):
async def run_benchmark(
max_conversations: int = None,
max_questions_per_conv: int = None,
skip_ingestion: bool = False
):
"""
Run the LoComo benchmark.
@ -406,81 +32,103 @@ def run_benchmark(max_conversations: int = None, max_questions_per_conv: int = N
max_questions_per_conv: Maximum questions per conversation (None for all)
skip_ingestion: Whether to skip ingestion and use existing data
"""
console.print("\n[bold cyan]LoComo Benchmark - Entity-Aware Memory System[/bold cyan]")
console.print("=" * 80)
# Load dataset
console.print("\n[1] Loading LoComo dataset...")
with open('locomo10.json', 'r') as f:
dataset = json.load(f)
conversations_to_eval = dataset[:max_conversations] if max_conversations else dataset
console.print(f" [green]✓[/green] Loaded {len(conversations_to_eval)} conversations")
# Initialize memory system
console.print("\n[2] Initializing memory system...")
# Initialize components
dataset = LoComoDataset()
answer_generator = LoComoAnswerGenerator()
answer_evaluator = LoComoAnswerEvaluator()
memory = TemporalSemanticMemory()
console.print(" [green]✓[/green] Memory system initialized")
# Run evaluation (conversations sequential, sessions within each conversation parallel)
all_results = []
for i, conv_data in enumerate(conversations_to_eval, 1):
result = asyncio.run(
process_single_conversation(
memory, conv_data, i, len(conversations_to_eval),
max_questions_per_conv, skip_ingestion
)
)
all_results.append(result)
# Overall results
console.print("\n[bold green]✓ Benchmark Complete![/bold green]\n")
# Calculate overall metrics
total_correct = sum(r['metrics']['correct'] for r in all_results)
total_questions = sum(r['metrics']['total'] for r in all_results)
overall_accuracy = (total_correct / total_questions * 100) if total_questions > 0 else 0
# Display results table
table = Table(title="LoComo Benchmark Results", box=box.ROUNDED)
table.add_column("Sample ID", style="cyan")
table.add_column("Turns", justify="right", style="yellow")
table.add_column("Questions", justify="right", style="blue")
table.add_column("Correct", justify="right", style="green")
table.add_column("Accuracy", justify="right", style="magenta")
for result in all_results:
metrics = result['metrics']
table.add_row(
result['sample_id'],
str(result['total_turns']),
str(metrics['total']),
str(metrics['correct']),
f"{metrics['accuracy']:.1f}%"
)
table.add_row(
"[bold]OVERALL[/bold]",
"-",
f"[bold]{total_questions}[/bold]",
f"[bold]{total_correct}[/bold]",
f"[bold]{overall_accuracy:.1f}%[/bold]"
# Create benchmark runner
runner = BenchmarkRunner(
dataset=dataset,
answer_generator=answer_generator,
answer_evaluator=answer_evaluator,
memory=memory
)
console.print(table)
# Run benchmark
dataset_path = Path(__file__).parent / 'locomo10.json'
results = await runner.run(
dataset_path=dataset_path,
agent_id="locomo",
max_items=max_conversations,
max_questions_per_item=max_questions_per_conv,
thinking_budget=500,
top_k=20,
skip_ingestion=skip_ingestion,
max_concurrent_questions=16,
eval_semaphore_size=8
)
return {
'overall_accuracy': overall_accuracy,
'total_correct': total_correct,
'total_questions': total_questions,
'conversation_results': all_results
# Display and save results
runner.display_results(results)
runner.save_results(results, Path(__file__).parent / 'benchmark_results.json')
# Generate markdown table
generate_markdown_table(results)
return results
def generate_markdown_table(results: dict):
"""
Generate a markdown table with benchmark results.
Category mapping:
1 = Multi-hop
2 = Single-hop
3 = Temporal
4 = Open-domain
"""
from rich.console import Console
console = Console()
category_names = {
'1': 'Multi-hop',
'2': 'Single-hop',
'3': 'Temporal',
'4': 'Open-domain'
}
# Build markdown content
lines = []
lines.append("# LoComo Benchmark Results")
lines.append("")
lines.append(f"**Overall Accuracy**: {results['overall_accuracy']:.2f}% ({results['total_correct']}/{results['total_questions']})")
lines.append("")
lines.append("| Sample ID | Sessions | Questions | Correct | Accuracy | Multi-hop | Single-hop | Temporal | Open-domain |")
lines.append("|-----------|----------|-----------|---------|----------|-----------|------------|----------|-------------|")
for item_result in results['item_results']:
item_id = item_result['item_id']
num_sessions = item_result['num_sessions']
metrics = item_result['metrics']
# Calculate category accuracies
cat_stats = metrics.get('category_stats', {})
cat_accuracies = {}
for cat_id in ['1', '2', '3', '4']:
if cat_id in cat_stats:
stats = cat_stats[cat_id]
acc = (stats['correct'] / stats['total'] * 100) if stats['total'] > 0 else 0
cat_accuracies[cat_id] = f"{acc:.1f}% ({stats['correct']}/{stats['total']})"
else:
cat_accuracies[cat_id] = "N/A"
lines.append(
f"| {item_id} | {num_sessions} | {metrics['total']} | {metrics['correct']} | "
f"{metrics['accuracy']:.2f}% | {cat_accuracies['1']} | {cat_accuracies['2']} | "
f"{cat_accuracies['3']} | {cat_accuracies['4']} |"
)
# Write to file
output_file = Path(__file__).parent / 'results_table.md'
output_file.write_text('\n'.join(lines))
console.print(f"\n[green]✓[/green] Results table saved to {output_file}")
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description='Run LoComo benchmark')
parser.add_argument('--max-conversations', type=int, default=None, help='Maximum conversations to evaluate')
parser.add_argument('--max-questions', type=int, default=None, help='Maximum questions per conversation')
@ -488,14 +136,8 @@ if __name__ == "__main__":
args = parser.parse_args()
results = run_benchmark(
results = asyncio.run(run_benchmark(
max_conversations=args.max_conversations,
max_questions_per_conv=args.max_questions,
skip_ingestion=args.skip_ingestion
)
# Save results
with open('benchmark_results.json', 'w') as f:
json.dump(results, f, indent=2)
console.print(f"\n[green]✓[/green] Results saved to benchmark_results.json")
))

View file

@ -0,0 +1,254 @@
"""
LongMemEval-specific benchmark implementations.
Provides dataset, answer generator, and evaluator for the LongMemEval benchmark.
"""
import sys
from pathlib import Path
# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
import json
from datetime import datetime, timezone
from typing import List, Dict, Any, Tuple, Optional
import asyncio
from openai import AsyncOpenAI
import os
from dotenv import load_dotenv
load_dotenv()
# Import common framework
sys.path.insert(0, str(Path(__file__).parent.parent))
from common.benchmark_runner import BenchmarkDataset, LLMAnswerGenerator, LLMAnswerEvaluator
class LongMemEvalDataset(BenchmarkDataset):
"""LongMemEval dataset implementation."""
def load(self, path: Path, max_items: Optional[int] = None) -> List[Dict[str, Any]]:
"""Load LongMemEval dataset from JSON file."""
with open(path, 'r') as f:
dataset = json.load(f)
if max_items:
dataset = dataset[:max_items]
return dataset
def get_item_id(self, item: Dict) -> str:
"""Get question ID from LongMemEval item."""
return item.get("question_id", "unknown")
def prepare_sessions_for_ingestion(self, item: Dict) -> List[Dict[str, Any]]:
"""
Prepare LongMemEval conversation sessions for batch ingestion.
Returns:
List of session dicts with 'content', 'context', 'event_date'
"""
sessions = item.get("haystack_sessions", [])
dates = item.get("haystack_dates", [])
session_ids = item.get("haystack_session_ids", [])
# Ensure all lists have same length
if not (len(sessions) == len(dates) == len(session_ids)):
min_len = min(len(sessions), len(dates), len(session_ids))
sessions = sessions[:min_len]
dates = dates[:min_len]
session_ids = session_ids[:min_len]
batch_contents = []
# Process each session
for session_turns, date_str, session_id in zip(sessions, dates, session_ids):
# Parse session date
session_date = self._parse_date(date_str) if date_str else datetime.now(timezone.utc)
# Combine all turns in the session into one content string
session_content_parts = []
for turn_dict in session_turns:
role = turn_dict.get("role", "")
content = turn_dict.get("content", "")
if not content.strip():
continue
# Format as "role: content"
session_content_parts.append(f"{role}: {content}")
# Add session to batch
if session_content_parts:
session_content = "\n".join(session_content_parts)
batch_contents.append({
"content": session_content,
"context": f"Session {session_id}",
"event_date": session_date
})
return batch_contents
def get_qa_pairs(self, item: Dict) -> List[Dict[str, Any]]:
"""
Extract QA pairs from LongMemEval item.
For LongMemEval, each item has one question.
Returns:
List with single QA dict with 'question', 'answer', 'category'
"""
return [{
'question': item.get("question", ""),
'answer': item.get("answer", ""),
'category': item.get("question_type", "unknown")
}]
def _parse_date(self, date_str: str) -> datetime:
"""Parse date string to datetime object."""
try:
# LongMemEval format: "2023/05/20 (Sat) 02:21"
# Try to parse the main part before the day name
date_str_cleaned = date_str.split('(')[0].strip() if '(' in date_str else date_str
# Try multiple formats
for fmt in ["%Y/%m/%d %H:%M", "%Y-%m-%d %H:%M:%S", "%Y-%m-%d", "%Y/%m/%d"]:
try:
dt = datetime.strptime(date_str_cleaned, fmt)
return dt.replace(tzinfo=timezone.utc)
except ValueError:
continue
# Fallback: try ISO format
return datetime.fromisoformat(date_str.replace('Z', '+00:00'))
except Exception:
return datetime.now(timezone.utc)
class LongMemEvalAnswerGenerator(LLMAnswerGenerator):
"""LongMemEval-specific answer generator using OpenAI."""
def __init__(self, model: str = "gpt-4o-mini"):
"""Initialize with OpenAI client."""
self.model = model
openai_api_key = os.getenv("OPENAI_API_KEY")
if not openai_api_key:
raise ValueError("OPENAI_API_KEY environment variable not set")
self.client = AsyncOpenAI(api_key=openai_api_key)
async def generate_answer(
self,
question: str,
memories: List[Dict[str, Any]]
) -> Tuple[str, str]:
"""
Generate answer from retrieved memories using OpenAI.
Returns:
Tuple of (answer, reasoning)
"""
# Format memories as context
context_parts = []
for i, mem in enumerate(memories, 1):
context_parts.append(f"[Memory {i}] {mem['text']}")
context = "\n".join(context_parts) if context_parts else "No relevant memories found."
prompt = f"""You are a helpful assistant. Based on the following memories from past conversations, answer the question.
Memories:
{context}
Question: {question}
Instructions:
- Answer based ONLY on the provided memories
- If the memories don't contain the answer, say "I don't have enough information to answer this question"
- Be concise and direct
- If asked to abstain (e.g., for unanswerable questions), explicitly say you cannot answer
Answer:"""
try:
response = await self.client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": prompt}],
temperature=0.0,
max_tokens=300
)
answer = response.choices[0].message.content.strip()
return answer, "" # LongMemEval doesn't use reasoning
except Exception as e:
return f"Error generating answer: {str(e)}", ""
class LongMemEvalAnswerEvaluator(LLMAnswerEvaluator):
"""LongMemEval-specific answer evaluator using OpenAI."""
def __init__(self, model: str = "gpt-4o"):
"""Initialize with OpenAI client."""
self.model = model
openai_api_key = os.getenv("OPENAI_API_KEY")
if not openai_api_key:
raise ValueError("OPENAI_API_KEY environment variable not set")
self.client = AsyncOpenAI(api_key=openai_api_key)
async def judge_answer(
self,
question: str,
correct_answer: str,
predicted_answer: str,
semaphore: asyncio.Semaphore
) -> Tuple[bool, str]:
"""
Evaluate predicted answer using OpenAI LLM-as-judge.
Returns:
Tuple of (is_correct, reasoning)
"""
async with semaphore:
prompt = f"""You are an expert evaluator. Evaluate if the predicted answer is semantically equivalent to the gold answer.
Question: {question}
Gold Answer: {correct_answer}
Predicted Answer: {predicted_answer}
Instructions:
- Score 1 if the predicted answer is semantically equivalent (same meaning, different wording is OK)
- Score 1 if the predicted answer correctly abstains when the gold answer indicates the question is unanswerable
- Score 0 if the predicted answer is incorrect or contradicts the gold answer
- Score 0 if the predicted answer provides an answer when it should abstain
- Provide a brief explanation
Output format:
Score: [0 or 1]
Explanation: [brief explanation]"""
try:
response = await self.client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": prompt}],
temperature=0.0,
max_tokens=200
)
content = response.choices[0].message.content.strip()
# Parse score and explanation
lines = content.split('\n')
score = 0
explanation = ""
for line in lines:
if line.startswith("Score:"):
score_str = line.replace("Score:", "").strip()
score = int(score_str) if score_str.isdigit() else 0
elif line.startswith("Explanation:"):
explanation = line.replace("Explanation:", "").strip()
return score == 1, explanation
except Exception as e:
return False, f"Evaluation error: {str(e)}"

View file

@ -11,70 +11,28 @@ which tests five core long-term memory abilities:
Dataset: LongMemEval-S (~115k tokens, ~40 sessions per instance, 500 questions)
Source: https://github.com/xiaowu0162/LongMemEval
Uses the common benchmark framework with LongMemEval-specific implementations.
"""
import json
import os
import sys
import argparse
from datetime import datetime, timezone
from typing import Dict, List, Any
from pathlib import Path
import time
import asyncio
import subprocess
from dotenv import load_dotenv
# Load environment variables from .env
load_dotenv()
# Add parent directory to path
# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
sys.path.insert(0, str(Path(__file__).parent.parent))
from memory import TemporalSemanticMemory
from openai import OpenAI
import asyncio
import argparse
import subprocess
from rich.console import Console
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TimeElapsedColumn
from rich.table import Table
from memory import TemporalSemanticMemory
from longmemeval_benchmark import LongMemEvalDataset, LongMemEvalAnswerGenerator, LongMemEvalAnswerEvaluator
from common.benchmark_runner import BenchmarkRunner
console = Console()
def parse_args():
parser = argparse.ArgumentParser(description="Run LongMemEval benchmark")
parser.add_argument(
"--max-instances",
type=int,
default=None,
help="Limit number of instances to evaluate (default: all 500)"
)
parser.add_argument(
"--max-questions",
type=int,
default=None,
help="Limit number of questions per instance (for quick testing)"
)
parser.add_argument(
"--output",
type=str,
default="benchmark_results.json",
help="Output file for results"
)
parser.add_argument(
"--thinking-budget",
type=int,
default=100,
help="Thinking budget for spreading activation search"
)
parser.add_argument(
"--top-k",
type=int,
default=20,
help="Number of memory units to retrieve per query"
)
return parser.parse_args()
def download_dataset(dataset_path: Path) -> bool:
"""
Download the LongMemEval dataset if it doesn't exist.
@ -112,256 +70,24 @@ def download_dataset(dataset_path: Path) -> bool:
return False
def load_dataset(dataset_path: str) -> List[Dict[str, Any]]:
"""Load LongMemEval dataset from JSON file."""
with open(dataset_path, 'r') as f:
data = json.load(f)
return data
def parse_date(date_str: str) -> datetime:
"""Parse date string to datetime object."""
try:
# LongMemEval format: "2023/05/20 (Sat) 02:21"
# Try to parse the main part before the day name
date_str_cleaned = date_str.split('(')[0].strip() if '(' in date_str else date_str
# Try multiple formats
for fmt in ["%Y/%m/%d %H:%M", "%Y-%m-%d %H:%M:%S", "%Y-%m-%d", "%Y/%m/%d"]:
try:
dt = datetime.strptime(date_str_cleaned, fmt)
return dt.replace(tzinfo=timezone.utc)
except ValueError:
continue
# Fallback: try ISO format
return datetime.fromisoformat(date_str.replace('Z', '+00:00'))
except Exception as e:
console.print(f"[yellow]Warning: Failed to parse date '{date_str}': {e}[/yellow]")
return datetime.now(timezone.utc)
async def ingest_conversation(memory: TemporalSemanticMemory, agent_id: str, instance: Dict[str, Any]) -> None:
async def run_benchmark(
max_instances: int = None,
max_questions_per_instance: int = None,
thinking_budget: int = 100,
top_k: int = 20,
skip_ingestion: bool = False
):
"""
Ingest conversation history into memory system.
Run the LongMemEval benchmark.
Args:
memory: Memory system instance
agent_id: Unique agent ID for this conversation
instance: LongMemEval instance containing haystack_sessions
max_instances: Maximum number of instances to evaluate (None for all)
max_questions_per_instance: Maximum questions per instance (for testing)
thinking_budget: Thinking budget for spreading activation search
top_k: Number of memory units to retrieve per query
skip_ingestion: Whether to skip ingestion and use existing data
"""
# LongMemEval format: list of sessions, each session is a list of turn dicts
sessions = instance.get("haystack_sessions", [])
dates = instance.get("haystack_dates", [])
session_ids = instance.get("haystack_session_ids", [])
# Ensure all lists have same length
if not (len(sessions) == len(dates) == len(session_ids)):
console.print(f"[yellow]Warning: Mismatched lengths - sessions:{len(sessions)}, dates:{len(dates)}, ids:{len(session_ids)}[/yellow]")
min_len = min(len(sessions), len(dates), len(session_ids))
sessions = sessions[:min_len]
dates = dates[:min_len]
session_ids = session_ids[:min_len]
# Process each session - combine all turns into one put_async call
for session_turns, date_str, session_id in zip(sessions, dates, session_ids):
# Parse session date
session_date = parse_date(date_str) if date_str else datetime.now(timezone.utc)
# Combine all turns in the session into one content string
session_content_parts = []
for turn_dict in session_turns:
role = turn_dict.get("role", "")
content = turn_dict.get("content", "")
if not content.strip():
continue
# Format as "role: content" for clarity
session_content_parts.append(f"{role}: {content}")
# Ingest entire session as one chunk
if session_content_parts:
session_content = "\n".join(session_content_parts)
context = f"Session {session_id}"
try:
await memory.put_async(
agent_id=agent_id,
content=session_content,
context=context,
event_date=session_date
)
except Exception as e:
console.print(f"[yellow]Warning: Failed to ingest session {session_id}: {e}[/yellow]")
async def retrieve_memories(
memory: TemporalSemanticMemory,
agent_id: str,
query: str,
thinking_budget: int,
top_k: int
) -> List[Dict[str, Any]]:
"""
Retrieve relevant memories for a query.
Args:
memory: Memory system instance
agent_id: Agent ID
query: Query text
thinking_budget: Thinking budget for search
top_k: Number of results to return
Returns:
List of retrieved memory units
"""
try:
results = await memory.search_async(
agent_id=agent_id,
query=query,
thinking_budget=thinking_budget,
top_k=top_k
)
return results
except Exception as e:
console.print(f"[yellow]Warning: Search failed: {e}[/yellow]")
return []
def generate_answer(
client: OpenAI,
question: str,
memories: List[Dict[str, Any]],
model: str = "gpt-4o-mini"
) -> str:
"""
Generate answer to question using retrieved memories.
Args:
client: OpenAI client
question: Question text
memories: Retrieved memory units
model: OpenAI model to use
Returns:
Generated answer
"""
# Format memories as context
context_parts = []
for i, mem in enumerate(memories, 1):
context_parts.append(f"[Memory {i}] {mem['text']}")
context = "\n".join(context_parts) if context_parts else "No relevant memories found."
prompt = f"""You are a helpful assistant. Based on the following memories from past conversations, answer the question.
Memories:
{context}
Question: {question}
Instructions:
- Answer based ONLY on the provided memories
- If the memories don't contain the answer, say "I don't have enough information to answer this question"
- Be concise and direct
- If asked to abstain (e.g., for unanswerable questions), explicitly say you cannot answer
Answer:"""
try:
response = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
temperature=0.0,
max_tokens=300
)
return response.choices[0].message.content.strip()
except Exception as e:
console.print(f"[yellow]Warning: Answer generation failed: {e}[/yellow]")
return "Error generating answer"
def evaluate_answer(
client: OpenAI,
question: str,
predicted_answer: str,
gold_answer: str,
model: str = "gpt-4o"
) -> Dict[str, Any]:
"""
Evaluate predicted answer against gold answer using LLM-as-judge.
Args:
client: OpenAI client
question: Question text
predicted_answer: Predicted answer
gold_answer: Gold answer
model: OpenAI model to use for evaluation
Returns:
Evaluation result with score and explanation
"""
prompt = f"""You are an expert evaluator. Evaluate if the predicted answer is semantically equivalent to the gold answer.
Question: {question}
Gold Answer: {gold_answer}
Predicted Answer: {predicted_answer}
Instructions:
- Score 1 if the predicted answer is semantically equivalent (same meaning, different wording is OK)
- Score 1 if the predicted answer correctly abstains when the gold answer indicates the question is unanswerable
- Score 0 if the predicted answer is incorrect or contradicts the gold answer
- Score 0 if the predicted answer provides an answer when it should abstain
- Provide a brief explanation
Output format:
Score: [0 or 1]
Explanation: [brief explanation]"""
try:
response = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
temperature=0.0,
max_tokens=200
)
content = response.choices[0].message.content.strip()
# Parse score and explanation
lines = content.split('\n')
score = 0
explanation = ""
for line in lines:
if line.startswith("Score:"):
score_str = line.replace("Score:", "").strip()
score = int(score_str) if score_str.isdigit() else 0
elif line.startswith("Explanation:"):
explanation = line.replace("Explanation:", "").strip()
return {
"score": score,
"explanation": explanation,
"raw_output": content
}
except Exception as e:
console.print(f"[yellow]Warning: Evaluation failed: {e}[/yellow]")
return {
"score": 0,
"explanation": f"Evaluation error: {str(e)}",
"raw_output": ""
}
def run_benchmark(args):
"""Run the LongMemEval benchmark evaluation."""
console.print("\n[bold cyan]LongMemEval Benchmark Evaluation[/bold cyan]\n")
# Load dataset - download if needed
# Check dataset exists, download if needed
dataset_path = Path(__file__).parent / "longmemeval_s_cleaned.json"
if not dataset_path.exists():
if not download_dataset(dataset_path):
@ -369,161 +95,120 @@ def run_benchmark(args):
console.print("[yellow]curl -L 'https://huggingface.co/datasets/xiaowu0162/longmemeval-cleaned/resolve/main/longmemeval_s_cleaned.json' -o benchmarks/longmemeval/longmemeval_s_cleaned.json[/yellow]")
return
console.print(f"[green]Loading dataset from {dataset_path}[/green]")
dataset = load_dataset(dataset_path)
if args.max_instances:
dataset = dataset[:args.max_instances]
console.print(f"[yellow]Limited to {args.max_instances} instances[/yellow]")
console.print(f"Dataset size: {len(dataset)} instances\n")
# Initialize memory system
console.print("[cyan]Initializing memory system...[/cyan]")
# Initialize components
dataset = LongMemEvalDataset()
answer_generator = LongMemEvalAnswerGenerator(model="gpt-4o-mini")
answer_evaluator = LongMemEvalAnswerEvaluator(model="gpt-4o")
memory = TemporalSemanticMemory()
# Initialize OpenAI client
openai_api_key = os.getenv("OPENAI_API_KEY")
if not openai_api_key:
console.print("[red]Error: OPENAI_API_KEY not set[/red]")
return
# Create benchmark runner
runner = BenchmarkRunner(
dataset=dataset,
answer_generator=answer_generator,
answer_evaluator=answer_evaluator,
memory=memory
)
client = OpenAI(api_key=openai_api_key)
# Run benchmark
# Note: LongMemEval requires clearing agent per item for isolation
results = await runner.run(
dataset_path=dataset_path,
agent_id="longmemeval",
max_items=max_instances,
max_questions_per_item=max_questions_per_instance,
thinking_budget=thinking_budget,
top_k=top_k,
skip_ingestion=skip_ingestion,
max_concurrent_questions=8, # Lower for LongMemEval (each has full conversation)
eval_semaphore_size=8,
clear_agent_per_item=True # Clear agent data per item for isolation
)
# Results storage
results = []
# Display and save results
runner.display_results(results)
runner.save_results(results, Path(__file__).parent / 'benchmark_results.json')
# Process each instance
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
BarColumn(),
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
TimeElapsedColumn(),
console=console
) as progress:
# Generate detailed report by question type
generate_type_report(results)
instance_task = progress.add_task("[cyan]Processing instances...", total=len(dataset))
for idx, instance in enumerate(dataset):
question_id = instance.get("question_id", f"q_{idx}")
question = instance.get("question", "")
gold_answer = instance.get("answer", "")
question_type = instance.get("question_type", "unknown")
progress.update(instance_task, description=f"[cyan]Instance {idx+1}/{len(dataset)}: {question_id}")
# Use single agent for all LongMemEval data (cleared per question for isolation)
agent_id = "longmemeval"
# Clear agent data for this question (each question needs fresh isolated context)
memory.delete_agent(agent_id)
# Ingest conversation history
try:
asyncio.run(ingest_conversation(memory, agent_id, instance))
except Exception as e:
console.print(f"[red]Error ingesting instance {question_id}: {e}[/red]")
continue
# Retrieve memories
memories = asyncio.run(retrieve_memories(
memory,
agent_id,
question,
args.thinking_budget,
args.top_k
))
# Generate answer
predicted_answer = generate_answer(client, question, memories)
# Evaluate answer
evaluation = evaluate_answer(client, question, predicted_answer, gold_answer)
# Store result
result = {
"question_id": question_id,
"question_type": question_type,
"question": question,
"gold_answer": gold_answer,
"predicted_answer": predicted_answer,
"score": evaluation["score"],
"explanation": evaluation["explanation"],
"num_memories_retrieved": len(memories),
"memory_texts": [m["text"] for m in memories[:5]] # Store top 5 for debugging
}
results.append(result)
progress.update(instance_task, advance=1)
# Save intermediate results
if (idx + 1) % 10 == 0:
save_results(results, args.output)
# Save final results
save_results(results, args.output)
# Display summary
display_summary(results)
return results
def save_results(results: List[Dict[str, Any]], output_path: str):
"""Save results to JSON file."""
output_file = Path(__file__).parent / output_path
with open(output_file, 'w') as f:
json.dump(results, f, indent=2)
console.print(f"[green]Results saved to {output_file}[/green]")
def generate_type_report(results: dict):
"""Generate a detailed report by question type."""
from rich.table import Table
def display_summary(results: List[Dict[str, Any]]):
"""Display benchmark summary."""
console.print("\n[bold cyan]Benchmark Summary[/bold cyan]\n")
# Overall accuracy
total = len(results)
correct = sum(1 for r in results if r["score"] == 1)
accuracy = (correct / total * 100) if total > 0 else 0
table = Table(title="Overall Performance")
table.add_column("Metric", style="cyan")
table.add_column("Value", style="green")
table.add_row("Total Questions", str(total))
table.add_row("Correct", str(correct))
table.add_row("Incorrect", str(total - correct))
table.add_row("Accuracy", f"{accuracy:.2f}%")
console.print(table)
# Accuracy by question type
# Aggregate stats by question type
type_stats = {}
for result in results:
qtype = result["question_type"]
if qtype not in type_stats:
type_stats[qtype] = {"total": 0, "correct": 0}
type_stats[qtype]["total"] += 1
type_stats[qtype]["correct"] += result["score"]
type_table = Table(title="Performance by Question Type")
type_table.add_column("Question Type", style="cyan")
type_table.add_column("Total", style="yellow")
type_table.add_column("Correct", style="green")
type_table.add_column("Accuracy", style="green")
for item_result in results['item_results']:
metrics = item_result['metrics']
by_category = metrics.get('category_stats', {})
for qtype, stats in by_category.items():
if qtype not in type_stats:
type_stats[qtype] = {'total': 0, 'correct': 0}
type_stats[qtype]['total'] += stats['total']
type_stats[qtype]['correct'] += stats['correct']
# Display table
table = Table(title="Performance by Question Type")
table.add_column("Question Type", style="cyan")
table.add_column("Total", justify="right", style="yellow")
table.add_column("Correct", justify="right", style="green")
table.add_column("Accuracy", justify="right", style="magenta")
for qtype, stats in sorted(type_stats.items()):
acc = (stats["correct"] / stats["total"] * 100) if stats["total"] > 0 else 0
type_table.add_row(
acc = (stats['correct'] / stats['total'] * 100) if stats['total'] > 0 else 0
table.add_row(
qtype,
str(stats["total"]),
str(stats["correct"]),
f"{acc:.2f}%"
str(stats['total']),
str(stats['correct']),
f"{acc:.1f}%"
)
console.print("\n")
console.print(type_table)
console.print(table)
if __name__ == "__main__":
args = parse_args()
run_benchmark(args)
parser = argparse.ArgumentParser(description="Run LongMemEval benchmark")
parser.add_argument(
"--max-instances",
type=int,
default=None,
help="Limit number of instances to evaluate (default: all 500)"
)
parser.add_argument(
"--max-questions",
type=int,
default=None,
help="Limit number of questions per instance (for quick testing)"
)
parser.add_argument(
"--thinking-budget",
type=int,
default=100,
help="Thinking budget for spreading activation search"
)
parser.add_argument(
"--top-k",
type=int,
default=20,
help="Number of memory units to retrieve per query"
)
parser.add_argument(
"--skip-ingestion",
action="store_true",
help="Skip ingestion and use existing data"
)
args = parser.parse_args()
results = asyncio.run(run_benchmark(
max_instances=args.max_instances,
max_questions_per_instance=args.max_questions,
thinking_budget=args.thinking_budget,
top_k=args.top_k,
skip_ingestion=args.skip_ingestion
))

180
examples/trace_example.py Normal file
View file

@ -0,0 +1,180 @@
"""
Example demonstrating search tracing functionality.
This script shows how to:
1. Enable search tracing
2. Retrieve the trace object
3. Export trace to JSON for visualization
"""
import asyncio
import json
from datetime import datetime, timezone
from memory import TemporalSemanticMemory
async def main():
"""Run the trace example."""
# Initialize memory system
memory = TemporalSemanticMemory()
try:
# Create a test agent
agent_id = f"trace_demo_{datetime.now(timezone.utc).timestamp()}"
print("=" * 70)
print("SEARCH TRACE EXAMPLE")
print("=" * 70)
# Store some test memories
print("\n1. Storing test memories...")
await memory.put_async(
agent_id=agent_id,
content="Alice works at Google as a software engineer in Mountain View",
context="conversation",
)
await memory.put_async(
agent_id=agent_id,
content="Bob also works at Google but in the New York office",
context="conversation",
)
await memory.put_async(
agent_id=agent_id,
content="Charlie founded TechCorp, a startup in San Francisco",
context="conversation",
)
await memory.put_async(
agent_id=agent_id,
content="Alice and Bob met at a Google conference last year",
context="conversation",
)
print(" ✓ 4 memories stored")
# Perform search with tracing enabled
print("\n2. Searching with trace enabled...")
query = "Who works at Google?"
results, trace = await memory.search_async(
agent_id=agent_id,
query=query,
thinking_budget=30,
top_k=5,
enable_trace=True,
)
print(f" ✓ Search completed")
# Display trace summary
print("\n3. Trace Summary:")
print(f" - Query: {trace.query.query_text}")
print(f" - Thinking budget: {trace.query.thinking_budget}")
print(f" - Entry points found: {len(trace.entry_points)}")
print(f" - Total nodes visited: {trace.summary.total_nodes_visited}")
print(f" - Total nodes pruned: {trace.summary.total_nodes_pruned}")
print(f" - Budget used: {trace.summary.budget_used}")
print(f" - Budget remaining: {trace.summary.budget_remaining}")
print(f" - Results returned: {trace.summary.results_returned}")
print(f" - Total duration: {trace.summary.total_duration_seconds:.3f}s")
print(f" - Temporal links followed: {trace.summary.temporal_links_followed}")
print(f" - Semantic links followed: {trace.summary.semantic_links_followed}")
print(f" - Entity links followed: {trace.summary.entity_links_followed}")
# Show entry points
print("\n4. Entry Points:")
for ep in trace.entry_points:
print(f" [{ep.rank}] {ep.text[:60]}... (similarity: {ep.similarity_score:.3f})")
# Show visited nodes with their paths
print("\n5. Search Path (First 5 visits):")
for i, visit in enumerate(trace.visits[:5], 1):
indent = " "
if visit.is_entry_point:
print(f"{indent}[{i}] ENTRY POINT: {visit.text[:60]}...")
else:
parent = f"from {visit.parent_node_id[:8]}" if visit.parent_node_id else "?"
link_info = f"via {visit.link_type}" if visit.link_type else ""
print(f"{indent}[{i}] {parent} {link_info}: {visit.text[:60]}...")
print(f"{indent} - Activation: {visit.weights.activation:.3f}")
print(f"{indent} - Semantic sim: {visit.weights.semantic_similarity:.3f}")
print(f"{indent} - Recency: {visit.weights.recency:.3f}")
print(f"{indent} - Final weight: {visit.weights.final_weight:.3f}")
if visit.neighbors_explored:
followed = sum(1 for n in visit.neighbors_explored if n.followed)
pruned = len(visit.neighbors_explored) - followed
print(f"{indent} - Neighbors: {followed} followed, {pruned} pruned")
# Show pruning decisions
if trace.pruned:
print(f"\n6. Pruning Decisions (showing first 5 of {len(trace.pruned)}):")
for prune in trace.pruned[:5]:
print(f" - Node {prune.node_id[:8]}: {prune.reason} (activation: {prune.activation:.3f})")
# Show phase metrics
print("\n7. Phase Metrics:")
for pm in trace.summary.phase_metrics:
print(f" - {pm.phase_name}: {pm.duration_seconds:.3f}s")
if pm.details:
for key, value in pm.details.items():
if isinstance(value, float):
print(f"{key}: {value:.3f}")
else:
print(f"{key}: {value}")
# Export to JSON
print("\n8. Exporting trace to JSON...")
trace_json = trace.to_json()
output_file = f"trace_{agent_id}.json"
with open(output_file, "w") as f:
f.write(trace_json)
print(f" ✓ Trace saved to: {output_file}")
print(f" ✓ JSON size: {len(trace_json):,} bytes")
# Show search results
print("\n9. Search Results:")
for i, result in enumerate(results, 1):
print(f" [{i}] {result['text'][:70]}...")
print(f" Weight: {result['weight']:.3f} "
f"(act: {result['activation']:.2f}, "
f"sem: {result['semantic_similarity']:.2f}, "
f"rec: {result['recency']:.2f})")
# Test helper methods
print("\n10. Testing Helper Methods:")
# Get path to first result
if results:
first_result_id = results[0]['id']
path = trace.get_search_path_to_node(first_result_id)
print(f" - Path to top result has {len(path)} steps")
# Count nodes by link type
temporal_nodes = trace.get_nodes_by_link_type("temporal")
semantic_nodes = trace.get_nodes_by_link_type("semantic")
entity_nodes = trace.get_nodes_by_link_type("entity")
print(f" - Nodes reached via temporal links: {len(temporal_nodes)}")
print(f" - Nodes reached via semantic links: {len(semantic_nodes)}")
print(f" - Nodes reached via entity links: {len(entity_nodes)}")
print("\n" + "=" * 70)
print("TRACE EXAMPLE COMPLETE!")
print("=" * 70)
print(f"\nYou can now build a visualization using the trace data in:")
print(f" {output_file}")
print("\nThe trace contains:")
print(f" - Complete search path with all nodes visited")
print(f" - Weight calculations for each node")
print(f" - Link information (type, weight, whether followed)")
print(f" - Pruning decisions with reasons")
print(f" - Performance metrics for each phase")
# Cleanup
print("\nCleaning up test agent...")
await memory.delete_agent(agent_id)
finally:
await memory.close()
if __name__ == "__main__":
asyncio.run(main())

View file

@ -1,189 +0,0 @@
function neighbourhoodHighlight(params) {
// console.log("in nieghbourhoodhighlight");
allNodes = nodes.get({ returnType: "Object" });
// originalNodes = JSON.parse(JSON.stringify(allNodes));
// if something is selected:
if (params.nodes.length > 0) {
highlightActive = true;
var i, j;
var selectedNode = params.nodes[0];
var degrees = 2;
// mark all nodes as hard to read.
for (let nodeId in allNodes) {
// nodeColors[nodeId] = allNodes[nodeId].color;
allNodes[nodeId].color = "rgba(200,200,200,0.5)";
if (allNodes[nodeId].hiddenLabel === undefined) {
allNodes[nodeId].hiddenLabel = allNodes[nodeId].label;
allNodes[nodeId].label = undefined;
}
}
var connectedNodes = network.getConnectedNodes(selectedNode);
var allConnectedNodes = [];
// get the second degree nodes
for (i = 1; i < degrees; i++) {
for (j = 0; j < connectedNodes.length; j++) {
allConnectedNodes = allConnectedNodes.concat(
network.getConnectedNodes(connectedNodes[j])
);
}
}
// all second degree nodes get a different color and their label back
for (i = 0; i < allConnectedNodes.length; i++) {
// allNodes[allConnectedNodes[i]].color = "pink";
allNodes[allConnectedNodes[i]].color = "rgba(150,150,150,0.75)";
if (allNodes[allConnectedNodes[i]].hiddenLabel !== undefined) {
allNodes[allConnectedNodes[i]].label =
allNodes[allConnectedNodes[i]].hiddenLabel;
allNodes[allConnectedNodes[i]].hiddenLabel = undefined;
}
}
// all first degree nodes get their own color and their label back
for (i = 0; i < connectedNodes.length; i++) {
// allNodes[connectedNodes[i]].color = undefined;
allNodes[connectedNodes[i]].color = nodeColors[connectedNodes[i]];
if (allNodes[connectedNodes[i]].hiddenLabel !== undefined) {
allNodes[connectedNodes[i]].label =
allNodes[connectedNodes[i]].hiddenLabel;
allNodes[connectedNodes[i]].hiddenLabel = undefined;
}
}
// the main node gets its own color and its label back.
// allNodes[selectedNode].color = undefined;
allNodes[selectedNode].color = nodeColors[selectedNode];
if (allNodes[selectedNode].hiddenLabel !== undefined) {
allNodes[selectedNode].label = allNodes[selectedNode].hiddenLabel;
allNodes[selectedNode].hiddenLabel = undefined;
}
} else if (highlightActive === true) {
// console.log("highlightActive was true");
// reset all nodes
for (let nodeId in allNodes) {
// allNodes[nodeId].color = "purple";
allNodes[nodeId].color = nodeColors[nodeId];
// delete allNodes[nodeId].color;
if (allNodes[nodeId].hiddenLabel !== undefined) {
allNodes[nodeId].label = allNodes[nodeId].hiddenLabel;
allNodes[nodeId].hiddenLabel = undefined;
}
}
highlightActive = false;
}
// transform the object into an array
var updateArray = [];
if (params.nodes.length > 0) {
for (let nodeId in allNodes) {
if (allNodes.hasOwnProperty(nodeId)) {
// console.log(allNodes[nodeId]);
updateArray.push(allNodes[nodeId]);
}
}
nodes.update(updateArray);
} else {
// console.log("Nothing was selected");
for (let nodeId in allNodes) {
if (allNodes.hasOwnProperty(nodeId)) {
// console.log(allNodes[nodeId]);
// allNodes[nodeId].color = {};
updateArray.push(allNodes[nodeId]);
}
}
nodes.update(updateArray);
}
}
function filterHighlight(params) {
allNodes = nodes.get({ returnType: "Object" });
// if something is selected:
if (params.nodes.length > 0) {
filterActive = true;
let selectedNodes = params.nodes;
// hiding all nodes and saving the label
for (let nodeId in allNodes) {
allNodes[nodeId].hidden = true;
if (allNodes[nodeId].savedLabel === undefined) {
allNodes[nodeId].savedLabel = allNodes[nodeId].label;
allNodes[nodeId].label = undefined;
}
}
for (let i=0; i < selectedNodes.length; i++) {
allNodes[selectedNodes[i]].hidden = false;
if (allNodes[selectedNodes[i]].savedLabel !== undefined) {
allNodes[selectedNodes[i]].label = allNodes[selectedNodes[i]].savedLabel;
allNodes[selectedNodes[i]].savedLabel = undefined;
}
}
} else if (filterActive === true) {
// reset all nodes
for (let nodeId in allNodes) {
allNodes[nodeId].hidden = false;
if (allNodes[nodeId].savedLabel !== undefined) {
allNodes[nodeId].label = allNodes[nodeId].savedLabel;
allNodes[nodeId].savedLabel = undefined;
}
}
filterActive = false;
}
// transform the object into an array
var updateArray = [];
if (params.nodes.length > 0) {
for (let nodeId in allNodes) {
if (allNodes.hasOwnProperty(nodeId)) {
updateArray.push(allNodes[nodeId]);
}
}
nodes.update(updateArray);
} else {
for (let nodeId in allNodes) {
if (allNodes.hasOwnProperty(nodeId)) {
updateArray.push(allNodes[nodeId]);
}
}
nodes.update(updateArray);
}
}
function selectNode(nodes) {
network.selectNodes(nodes);
neighbourhoodHighlight({ nodes: nodes });
return nodes;
}
function selectNodes(nodes) {
network.selectNodes(nodes);
filterHighlight({nodes: nodes});
return nodes;
}
function highlightFilter(filter) {
let selectedNodes = []
let selectedProp = filter['property']
if (filter['item'] === 'node') {
let allNodes = nodes.get({ returnType: "Object" });
for (let nodeId in allNodes) {
if (allNodes[nodeId][selectedProp] && filter['value'].includes((allNodes[nodeId][selectedProp]).toString())) {
selectedNodes.push(nodeId)
}
}
}
else if (filter['item'] === 'edge'){
let allEdges = edges.get({returnType: 'object'});
// check if the selected property exists for selected edge and select the nodes connected to the edge
for (let edge in allEdges) {
if (allEdges[edge][selectedProp] && filter['value'].includes((allEdges[edge][selectedProp]).toString())) {
selectedNodes.push(allEdges[edge]['from'])
selectedNodes.push(allEdges[edge]['to'])
}
}
}
selectNodes(selectedNodes)
}

View file

@ -1,356 +0,0 @@
/**
* Tom Select v2.0.0-rc.4
* Licensed under the Apache License, Version 2.0 (the "License");
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).TomSelect=t()}(this,(function(){"use strict"
function e(e,t){e.split(/\s+/).forEach((e=>{t(e)}))}class t{constructor(){this._events={}}on(t,i){e(t,(e=>{this._events[e]=this._events[e]||[],this._events[e].push(i)}))}off(t,i){var s=arguments.length
0!==s?e(t,(e=>{if(1===s)return delete this._events[e]
e in this._events!=!1&&this._events[e].splice(this._events[e].indexOf(i),1)})):this._events={}}trigger(t,...i){var s=this
e(t,(e=>{if(e in s._events!=!1)for(let t of s._events[e])t.apply(s,i)}))}}var i
const s="[̀-ͯ·ʾ]",n=new RegExp(s,"g")
var o
const r={"æ":"ae","ⱥ":"a","ø":"o"},l=new RegExp(Object.keys(r).join("|"),"g"),a=[[67,67],[160,160],[192,438],[452,652],[961,961],[1019,1019],[1083,1083],[1281,1289],[1984,1984],[5095,5095],[7429,7441],[7545,7549],[7680,7935],[8580,8580],[9398,9449],[11360,11391],[42792,42793],[42802,42851],[42873,42897],[42912,42922],[64256,64260],[65313,65338],[65345,65370]],c=e=>e.normalize("NFKD").replace(n,"").toLowerCase().replace(l,(function(e){return r[e]})),d=(e,t="|")=>{if(1==e.length)return e[0]
var i=1
return e.forEach((e=>{i=Math.max(i,e.length)})),1==i?"["+e.join("")+"]":"(?:"+e.join(t)+")"},p=e=>{if(1===e.length)return[[e]]
var t=[]
return p(e.substring(1)).forEach((function(i){var s=i.slice(0)
s[0]=e.charAt(0)+s[0],t.push(s),(s=i.slice(0)).unshift(e.charAt(0)),t.push(s)})),t},u=e=>{void 0===o&&(o=(()=>{var e={}
a.forEach((t=>{for(let s=t[0];s<=t[1];s++){let t=String.fromCharCode(s),n=c(t)
if(n!=t.toLowerCase()){n in e||(e[n]=[n])
var i=new RegExp(d(e[n]),"iu")
t.match(i)||e[n].push(t)}}}))
var t=Object.keys(e)
t=t.sort(((e,t)=>t.length-e.length)),i=new RegExp("("+d(t)+"[̀-ͯ·ʾ]*)","g")
var s={}
return t.sort(((e,t)=>e.length-t.length)).forEach((t=>{var i=p(t).map((t=>(t=t.map((t=>e.hasOwnProperty(t)?d(e[t]):t)),d(t,""))))
s[t]=d(i)})),s})())
return e.normalize("NFKD").toLowerCase().split(i).map((e=>{if(""==e)return""
const t=c(e)
if(o.hasOwnProperty(t))return o[t]
const i=e.normalize("NFC")
return i!=e?d([e,i]):e})).join("")},h=(e,t)=>{if(e)return e[t]},g=(e,t)=>{if(e){for(var i,s=t.split(".");(i=s.shift())&&(e=e[i]););return e}},f=(e,t,i)=>{var s,n
return e?-1===(n=(e+="").search(t.regex))?0:(s=t.string.length/e.length,0===n&&(s+=.5),s*i):0},v=e=>(e+"").replace(/([\$\(-\+\.\?\[-\^\{-\}])/g,"\\$1"),m=(e,t)=>{var i=e[t]
if("function"==typeof i)return i
i&&!Array.isArray(i)&&(e[t]=[i])},y=(e,t)=>{if(Array.isArray(e))e.forEach(t)
else for(var i in e)e.hasOwnProperty(i)&&t(e[i],i)},O=(e,t)=>"number"==typeof e&&"number"==typeof t?e>t?1:e<t?-1:0:(e=c(e+"").toLowerCase())>(t=c(t+"").toLowerCase())?1:t>e?-1:0
class b{constructor(e,t){this.items=e,this.settings=t||{diacritics:!0}}tokenize(e,t,i){if(!e||!e.length)return[]
const s=[],n=e.split(/\s+/)
var o
return i&&(o=new RegExp("^("+Object.keys(i).map(v).join("|")+"):(.*)$")),n.forEach((e=>{let i,n=null,r=null
o&&(i=e.match(o))&&(n=i[1],e=i[2]),e.length>0&&(r=v(e),this.settings.diacritics&&(r=u(r)),t&&(r="\\b"+r)),s.push({string:e,regex:r?new RegExp(r,"iu"):null,field:n})})),s}getScoreFunction(e,t){var i=this.prepareSearch(e,t)
return this._getScoreFunction(i)}_getScoreFunction(e){const t=e.tokens,i=t.length
if(!i)return function(){return 0}
const s=e.options.fields,n=e.weights,o=s.length,r=e.getAttrFn
if(!o)return function(){return 1}
const l=1===o?function(e,t){const i=s[0].field
return f(r(t,i),e,n[i])}:function(e,t){var i=0
if(e.field){const s=r(t,e.field)
!e.regex&&s?i+=1/o:i+=f(s,e,1)}else y(n,((s,n)=>{i+=f(r(t,n),e,s)}))
return i/o}
return 1===i?function(e){return l(t[0],e)}:"and"===e.options.conjunction?function(e){for(var s,n=0,o=0;n<i;n++){if((s=l(t[n],e))<=0)return 0
o+=s}return o/i}:function(e){var s=0
return y(t,(t=>{s+=l(t,e)})),s/i}}getSortFunction(e,t){var i=this.prepareSearch(e,t)
return this._getSortFunction(i)}_getSortFunction(e){var t,i,s
const n=this,o=e.options,r=!e.query&&o.sort_empty?o.sort_empty:o.sort,l=[],a=[]
if("function"==typeof r)return r.bind(this)
const c=function(t,i){return"$score"===t?i.score:e.getAttrFn(n.items[i.id],t)}
if(r)for(t=0,i=r.length;t<i;t++)(e.query||"$score"!==r[t].field)&&l.push(r[t])
if(e.query){for(s=!0,t=0,i=l.length;t<i;t++)if("$score"===l[t].field){s=!1
break}s&&l.unshift({field:"$score",direction:"desc"})}else for(t=0,i=l.length;t<i;t++)if("$score"===l[t].field){l.splice(t,1)
break}for(t=0,i=l.length;t<i;t++)a.push("desc"===l[t].direction?-1:1)
const d=l.length
if(d){if(1===d){const e=l[0].field,t=a[0]
return function(i,s){return t*O(c(e,i),c(e,s))}}return function(e,t){var i,s,n
for(i=0;i<d;i++)if(n=l[i].field,s=a[i]*O(c(n,e),c(n,t)))return s
return 0}}return null}prepareSearch(e,t){const i={}
var s=Object.assign({},t)
if(m(s,"sort"),m(s,"sort_empty"),s.fields){m(s,"fields")
const e=[]
s.fields.forEach((t=>{"string"==typeof t&&(t={field:t,weight:1}),e.push(t),i[t.field]="weight"in t?t.weight:1})),s.fields=e}return{options:s,query:e.toLowerCase().trim(),tokens:this.tokenize(e,s.respect_word_boundaries,i),total:0,items:[],weights:i,getAttrFn:s.nesting?g:h}}search(e,t){var i,s,n=this
s=this.prepareSearch(e,t),t=s.options,e=s.query
const o=t.score||n._getScoreFunction(s)
e.length?y(n.items,((e,n)=>{i=o(e),(!1===t.filter||i>0)&&s.items.push({score:i,id:n})})):y(n.items,((e,t)=>{s.items.push({score:1,id:t})}))
const r=n._getSortFunction(s)
return r&&s.items.sort(r),s.total=s.items.length,"number"==typeof t.limit&&(s.items=s.items.slice(0,t.limit)),s}}const w=e=>{if(e.jquery)return e[0]
if(e instanceof HTMLElement)return e
if(e.indexOf("<")>-1){let t=document.createElement("div")
return t.innerHTML=e.trim(),t.firstChild}return document.querySelector(e)},_=(e,t)=>{var i=document.createEvent("HTMLEvents")
i.initEvent(t,!0,!1),e.dispatchEvent(i)},I=(e,t)=>{Object.assign(e.style,t)},C=(e,...t)=>{var i=A(t);(e=x(e)).map((e=>{i.map((t=>{e.classList.add(t)}))}))},S=(e,...t)=>{var i=A(t);(e=x(e)).map((e=>{i.map((t=>{e.classList.remove(t)}))}))},A=e=>{var t=[]
return y(e,(e=>{"string"==typeof e&&(e=e.trim().split(/[\11\12\14\15\40]/)),Array.isArray(e)&&(t=t.concat(e))})),t.filter(Boolean)},x=e=>(Array.isArray(e)||(e=[e]),e),k=(e,t,i)=>{if(!i||i.contains(e))for(;e&&e.matches;){if(e.matches(t))return e
e=e.parentNode}},F=(e,t=0)=>t>0?e[e.length-1]:e[0],L=(e,t)=>{if(!e)return-1
t=t||e.nodeName
for(var i=0;e=e.previousElementSibling;)e.matches(t)&&i++
return i},P=(e,t)=>{y(t,((t,i)=>{null==t?e.removeAttribute(i):e.setAttribute(i,""+t)}))},E=(e,t)=>{e.parentNode&&e.parentNode.replaceChild(t,e)},T=(e,t)=>{if(null===t)return
if("string"==typeof t){if(!t.length)return
t=new RegExp(t,"i")}const i=e=>3===e.nodeType?(e=>{var i=e.data.match(t)
if(i&&e.data.length>0){var s=document.createElement("span")
s.className="highlight"
var n=e.splitText(i.index)
n.splitText(i[0].length)
var o=n.cloneNode(!0)
return s.appendChild(o),E(n,s),1}return 0})(e):((e=>{if(1===e.nodeType&&e.childNodes&&!/(script|style)/i.test(e.tagName)&&("highlight"!==e.className||"SPAN"!==e.tagName))for(var t=0;t<e.childNodes.length;++t)t+=i(e.childNodes[t])})(e),0)
i(e)},V="undefined"!=typeof navigator&&/Mac/.test(navigator.userAgent)?"metaKey":"ctrlKey"
var j={options:[],optgroups:[],plugins:[],delimiter:",",splitOn:null,persist:!0,diacritics:!0,create:null,createOnBlur:!1,createFilter:null,highlight:!0,openOnFocus:!0,shouldOpen:null,maxOptions:50,maxItems:null,hideSelected:null,duplicates:!1,addPrecedence:!1,selectOnTab:!1,preload:null,allowEmptyOption:!1,loadThrottle:300,loadingClass:"loading",dataAttr:null,optgroupField:"optgroup",valueField:"value",labelField:"text",disabledField:"disabled",optgroupLabelField:"label",optgroupValueField:"value",lockOptgroupOrder:!1,sortField:"$order",searchField:["text"],searchConjunction:"and",mode:null,wrapperClass:"ts-wrapper",controlClass:"ts-control",dropdownClass:"ts-dropdown",dropdownContentClass:"ts-dropdown-content",itemClass:"item",optionClass:"option",dropdownParent:null,copyClassesToDropdown:!1,placeholder:null,hidePlaceholder:null,shouldLoad:function(e){return e.length>0},render:{}}
const q=e=>null==e?null:D(e),D=e=>"boolean"==typeof e?e?"1":"0":e+"",N=e=>(e+"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"),z=(e,t)=>{var i
return function(s,n){var o=this
i&&(o.loading=Math.max(o.loading-1,0),clearTimeout(i)),i=setTimeout((function(){i=null,o.loadedSearches[s]=!0,e.call(o,s,n)}),t)}},R=(e,t,i)=>{var s,n=e.trigger,o={}
for(s in e.trigger=function(){var i=arguments[0]
if(-1===t.indexOf(i))return n.apply(e,arguments)
o[i]=arguments},i.apply(e,[]),e.trigger=n,o)n.apply(e,o[s])},H=(e,t=!1)=>{e&&(e.preventDefault(),t&&e.stopPropagation())},B=(e,t,i,s)=>{e.addEventListener(t,i,s)},K=(e,t)=>!!t&&(!!t[e]&&1===(t.altKey?1:0)+(t.ctrlKey?1:0)+(t.shiftKey?1:0)+(t.metaKey?1:0)),M=(e,t)=>{const i=e.getAttribute("id")
return i||(e.setAttribute("id",t),t)},Q=e=>e.replace(/[\\"']/g,"\\$&"),G=(e,t)=>{t&&e.append(t)}
function U(e,t){var i=Object.assign({},j,t),s=i.dataAttr,n=i.labelField,o=i.valueField,r=i.disabledField,l=i.optgroupField,a=i.optgroupLabelField,c=i.optgroupValueField,d=e.tagName.toLowerCase(),p=e.getAttribute("placeholder")||e.getAttribute("data-placeholder")
if(!p&&!i.allowEmptyOption){let t=e.querySelector('option[value=""]')
t&&(p=t.textContent)}var u,h,g,f,v,m,O={placeholder:p,options:[],optgroups:[],items:[],maxItems:null}
return"select"===d?(h=O.options,g={},f=1,v=e=>{var t=Object.assign({},e.dataset),i=s&&t[s]
return"string"==typeof i&&i.length&&(t=Object.assign(t,JSON.parse(i))),t},m=(e,t)=>{var s=q(e.value)
if(null!=s&&(s||i.allowEmptyOption)){if(g.hasOwnProperty(s)){if(t){var a=g[s][l]
a?Array.isArray(a)?a.push(t):g[s][l]=[a,t]:g[s][l]=t}}else{var c=v(e)
c[n]=c[n]||e.textContent,c[o]=c[o]||s,c[r]=c[r]||e.disabled,c[l]=c[l]||t,c.$option=e,g[s]=c,h.push(c)}e.selected&&O.items.push(s)}},O.maxItems=e.hasAttribute("multiple")?null:1,y(e.children,(e=>{var t,i,s
"optgroup"===(u=e.tagName.toLowerCase())?((s=v(t=e))[a]=s[a]||t.getAttribute("label")||"",s[c]=s[c]||f++,s[r]=s[r]||t.disabled,O.optgroups.push(s),i=s[c],y(t.children,(e=>{m(e,i)}))):"option"===u&&m(e)}))):(()=>{const t=e.getAttribute(s)
if(t)O.options=JSON.parse(t),y(O.options,(e=>{O.items.push(e[o])}))
else{var r=e.value.trim()||""
if(!i.allowEmptyOption&&!r.length)return
const t=r.split(i.delimiter)
y(t,(e=>{const t={}
t[n]=e,t[o]=e,O.options.push(t)})),O.items=t}})(),Object.assign({},j,O,t)}var W=0
class J extends(function(e){return e.plugins={},class extends e{constructor(...e){super(...e),this.plugins={names:[],settings:{},requested:{},loaded:{}}}static define(t,i){e.plugins[t]={name:t,fn:i}}initializePlugins(e){var t,i
const s=this,n=[]
if(Array.isArray(e))e.forEach((e=>{"string"==typeof e?n.push(e):(s.plugins.settings[e.name]=e.options,n.push(e.name))}))
else if(e)for(t in e)e.hasOwnProperty(t)&&(s.plugins.settings[t]=e[t],n.push(t))
for(;i=n.shift();)s.require(i)}loadPlugin(t){var i=this,s=i.plugins,n=e.plugins[t]
if(!e.plugins.hasOwnProperty(t))throw new Error('Unable to find "'+t+'" plugin')
s.requested[t]=!0,s.loaded[t]=n.fn.apply(i,[i.plugins.settings[t]||{}]),s.names.push(t)}require(e){var t=this,i=t.plugins
if(!t.plugins.loaded.hasOwnProperty(e)){if(i.requested[e])throw new Error('Plugin has circular dependency ("'+e+'")')
t.loadPlugin(e)}return i.loaded[e]}}}(t)){constructor(e,t){var i
super(),this.order=0,this.isOpen=!1,this.isDisabled=!1,this.isInvalid=!1,this.isValid=!0,this.isLocked=!1,this.isFocused=!1,this.isInputHidden=!1,this.isSetup=!1,this.ignoreFocus=!1,this.hasOptions=!1,this.lastValue="",this.caretPos=0,this.loading=0,this.loadedSearches={},this.activeOption=null,this.activeItems=[],this.optgroups={},this.options={},this.userOptions={},this.items=[],W++
var s=w(e)
if(s.tomselect)throw new Error("Tom Select already initialized on this element")
s.tomselect=this,i=(window.getComputedStyle&&window.getComputedStyle(s,null)).getPropertyValue("direction")
const n=U(s,t)
this.settings=n,this.input=s,this.tabIndex=s.tabIndex||0,this.is_select_tag="select"===s.tagName.toLowerCase(),this.rtl=/rtl/i.test(i),this.inputId=M(s,"tomselect-"+W),this.isRequired=s.required,this.sifter=new b(this.options,{diacritics:n.diacritics}),n.mode=n.mode||(1===n.maxItems?"single":"multi"),"boolean"!=typeof n.hideSelected&&(n.hideSelected="multi"===n.mode),"boolean"!=typeof n.hidePlaceholder&&(n.hidePlaceholder="multi"!==n.mode)
var o=n.createFilter
"function"!=typeof o&&("string"==typeof o&&(o=new RegExp(o)),o instanceof RegExp?n.createFilter=e=>o.test(e):n.createFilter=()=>!0),this.initializePlugins(n.plugins),this.setupCallbacks(),this.setupTemplates()
const r=w("<div>"),l=w("<div>"),a=this._render("dropdown"),c=w('<div role="listbox" tabindex="-1">'),d=this.input.getAttribute("class")||"",p=n.mode
var u
if(C(r,n.wrapperClass,d,p),C(l,n.controlClass),G(r,l),C(a,n.dropdownClass,p),n.copyClassesToDropdown&&C(a,d),C(c,n.dropdownContentClass),G(a,c),w(n.dropdownParent||r).appendChild(a),n.hasOwnProperty("controlInput"))n.controlInput?(u=w(n.controlInput),this.focus_node=u):(u=w("<input/>"),this.focus_node=l)
else{u=w('<input type="text" autocomplete="off" size="1" />')
y(["autocorrect","autocapitalize","autocomplete"],(e=>{s.getAttribute(e)&&P(u,{[e]:s.getAttribute(e)})})),u.tabIndex=-1,l.appendChild(u),this.focus_node=u}this.wrapper=r,this.dropdown=a,this.dropdown_content=c,this.control=l,this.control_input=u,this.setup()}setup(){const e=this,t=e.settings,i=e.control_input,s=e.dropdown,n=e.dropdown_content,o=e.wrapper,r=e.control,l=e.input,a=e.focus_node,c={passive:!0},d=e.inputId+"-ts-dropdown"
P(n,{id:d}),P(a,{role:"combobox","aria-haspopup":"listbox","aria-expanded":"false","aria-controls":d})
const p=M(a,e.inputId+"-ts-control"),u="label[for='"+(e=>e.replace(/['"\\]/g,"\\$&"))(e.inputId)+"']",h=document.querySelector(u),g=e.focus.bind(e)
if(h){B(h,"click",g),P(h,{for:p})
const t=M(h,e.inputId+"-ts-label")
P(a,{"aria-labelledby":t}),P(n,{"aria-labelledby":t})}if(o.style.width=l.style.width,e.plugins.names.length){const t="plugin-"+e.plugins.names.join(" plugin-")
C([o,s],t)}(null===t.maxItems||t.maxItems>1)&&e.is_select_tag&&P(l,{multiple:"multiple"}),e.settings.placeholder&&P(i,{placeholder:t.placeholder}),!e.settings.splitOn&&e.settings.delimiter&&(e.settings.splitOn=new RegExp("\\s*"+v(e.settings.delimiter)+"+\\s*")),t.load&&t.loadThrottle&&(t.load=z(t.load,t.loadThrottle)),e.control_input.type=l.type,B(s,"click",(t=>{const i=k(t.target,"[data-selectable]")
i&&(e.onOptionSelect(t,i),H(t,!0))})),B(r,"click",(t=>{var s=k(t.target,"[data-ts-item]",r)
s&&e.onItemSelect(t,s)?H(t,!0):""==i.value&&(e.onClick(),H(t,!0))})),B(i,"mousedown",(e=>{""!==i.value&&e.stopPropagation()})),B(a,"keydown",(t=>e.onKeyDown(t))),B(i,"keypress",(t=>e.onKeyPress(t))),B(i,"input",(t=>e.onInput(t))),B(a,"resize",(()=>e.positionDropdown()),c),B(a,"blur",(t=>e.onBlur(t))),B(a,"focus",(t=>e.onFocus(t))),B(a,"paste",(t=>e.onPaste(t)))
const f=t=>{const i=t.composedPath()[0]
if(!o.contains(i)&&!s.contains(i))return e.isFocused&&e.blur(),void e.inputState()
H(t,!0)}
var m=()=>{e.isOpen&&e.positionDropdown()}
B(document,"mousedown",f),B(window,"scroll",m,c),B(window,"resize",m,c),this._destroy=()=>{document.removeEventListener("mousedown",f),window.removeEventListener("sroll",m),window.removeEventListener("resize",m),h&&h.removeEventListener("click",g)},this.revertSettings={innerHTML:l.innerHTML,tabIndex:l.tabIndex},l.tabIndex=-1,l.insertAdjacentElement("afterend",e.wrapper),e.sync(!1),t.items=[],delete t.optgroups,delete t.options,B(l,"invalid",(t=>{e.isValid&&(e.isValid=!1,e.isInvalid=!0,e.refreshState())})),e.updateOriginalInput(),e.refreshItems(),e.close(!1),e.inputState(),e.isSetup=!0,l.disabled?e.disable():e.enable(),e.on("change",this.onChange),C(l,"tomselected","ts-hidden-accessible"),e.trigger("initialize"),!0===t.preload&&e.preload()}setupOptions(e=[],t=[]){this.addOptions(e),y(t,(e=>{this.registerOptionGroup(e)}))}setupTemplates(){var e=this,t=e.settings.labelField,i=e.settings.optgroupLabelField,s={optgroup:e=>{let t=document.createElement("div")
return t.className="optgroup",t.appendChild(e.options),t},optgroup_header:(e,t)=>'<div class="optgroup-header">'+t(e[i])+"</div>",option:(e,i)=>"<div>"+i(e[t])+"</div>",item:(e,i)=>"<div>"+i(e[t])+"</div>",option_create:(e,t)=>'<div class="create">Add <strong>'+t(e.input)+"</strong>&hellip;</div>",no_results:()=>'<div class="no-results">No results found</div>',loading:()=>'<div class="spinner"></div>',not_loading:()=>{},dropdown:()=>"<div></div>"}
e.settings.render=Object.assign({},s,e.settings.render)}setupCallbacks(){var e,t,i={initialize:"onInitialize",change:"onChange",item_add:"onItemAdd",item_remove:"onItemRemove",item_select:"onItemSelect",clear:"onClear",option_add:"onOptionAdd",option_remove:"onOptionRemove",option_clear:"onOptionClear",optgroup_add:"onOptionGroupAdd",optgroup_remove:"onOptionGroupRemove",optgroup_clear:"onOptionGroupClear",dropdown_open:"onDropdownOpen",dropdown_close:"onDropdownClose",type:"onType",load:"onLoad",focus:"onFocus",blur:"onBlur"}
for(e in i)(t=this.settings[i[e]])&&this.on(e,t)}sync(e=!0){const t=this,i=e?U(t.input,{delimiter:t.settings.delimiter}):t.settings
t.setupOptions(i.options,i.optgroups),t.setValue(i.items,!0),t.lastQuery=null}onClick(){var e=this
if(e.activeItems.length>0)return e.clearActiveItems(),void e.focus()
e.isFocused&&e.isOpen?e.blur():e.focus()}onMouseDown(){}onChange(){_(this.input,"input"),_(this.input,"change")}onPaste(e){var t=this
t.isFull()||t.isInputHidden||t.isLocked?H(e):t.settings.splitOn&&setTimeout((()=>{var e=t.inputValue()
if(e.match(t.settings.splitOn)){var i=e.trim().split(t.settings.splitOn)
y(i,(e=>{t.createItem(e)}))}}),0)}onKeyPress(e){var t=this
if(!t.isLocked){var i=String.fromCharCode(e.keyCode||e.which)
return t.settings.create&&"multi"===t.settings.mode&&i===t.settings.delimiter?(t.createItem(),void H(e)):void 0}H(e)}onKeyDown(e){var t=this
if(t.isLocked)9!==e.keyCode&&H(e)
else{switch(e.keyCode){case 65:if(K(V,e))return H(e),void t.selectAll()
break
case 27:return t.isOpen&&(H(e,!0),t.close()),void t.clearActiveItems()
case 40:if(!t.isOpen&&t.hasOptions)t.open()
else if(t.activeOption){let e=t.getAdjacent(t.activeOption,1)
e&&t.setActiveOption(e)}return void H(e)
case 38:if(t.activeOption){let e=t.getAdjacent(t.activeOption,-1)
e&&t.setActiveOption(e)}return void H(e)
case 13:return void(t.isOpen&&t.activeOption?(t.onOptionSelect(e,t.activeOption),H(e)):t.settings.create&&t.createItem()&&H(e))
case 37:return void t.advanceSelection(-1,e)
case 39:return void t.advanceSelection(1,e)
case 9:return void(t.settings.selectOnTab&&(t.isOpen&&t.activeOption&&(t.onOptionSelect(e,t.activeOption),H(e)),t.settings.create&&t.createItem()&&H(e)))
case 8:case 46:return void t.deleteSelection(e)}t.isInputHidden&&!K(V,e)&&H(e)}}onInput(e){var t=this
if(!t.isLocked){var i=t.inputValue()
t.lastValue!==i&&(t.lastValue=i,t.settings.shouldLoad.call(t,i)&&t.load(i),t.refreshOptions(),t.trigger("type",i))}}onFocus(e){var t=this,i=t.isFocused
if(t.isDisabled)return t.blur(),void H(e)
t.ignoreFocus||(t.isFocused=!0,"focus"===t.settings.preload&&t.preload(),i||t.trigger("focus"),t.activeItems.length||(t.showInput(),t.refreshOptions(!!t.settings.openOnFocus)),t.refreshState())}onBlur(e){if(!1!==document.hasFocus()){var t=this
if(t.isFocused){t.isFocused=!1,t.ignoreFocus=!1
var i=()=>{t.close(),t.setActiveItem(),t.setCaret(t.items.length),t.trigger("blur")}
t.settings.create&&t.settings.createOnBlur?t.createItem(null,!1,i):i()}}}onOptionSelect(e,t){var i,s=this
t&&(t.parentElement&&t.parentElement.matches("[data-disabled]")||(t.classList.contains("create")?s.createItem(null,!0,(()=>{s.settings.closeAfterSelect&&s.close()})):void 0!==(i=t.dataset.value)&&(s.lastQuery=null,s.addItem(i),s.settings.closeAfterSelect&&s.close(),!s.settings.hideSelected&&e.type&&/click/.test(e.type)&&s.setActiveOption(t))))}onItemSelect(e,t){var i=this
return!i.isLocked&&"multi"===i.settings.mode&&(H(e),i.setActiveItem(t,e),!0)}canLoad(e){return!!this.settings.load&&!this.loadedSearches.hasOwnProperty(e)}load(e){const t=this
if(!t.canLoad(e))return
C(t.wrapper,t.settings.loadingClass),t.loading++
const i=t.loadCallback.bind(t)
t.settings.load.call(t,e,i)}loadCallback(e,t){const i=this
i.loading=Math.max(i.loading-1,0),i.lastQuery=null,i.clearActiveOption(),i.setupOptions(e,t),i.refreshOptions(i.isFocused&&!i.isInputHidden),i.loading||S(i.wrapper,i.settings.loadingClass),i.trigger("load",e,t)}preload(){var e=this.wrapper.classList
e.contains("preloaded")||(e.add("preloaded"),this.load(""))}setTextboxValue(e=""){var t=this.control_input
t.value!==e&&(t.value=e,_(t,"update"),this.lastValue=e)}getValue(){return this.is_select_tag&&this.input.hasAttribute("multiple")?this.items:this.items.join(this.settings.delimiter)}setValue(e,t){R(this,t?[]:["change"],(()=>{this.clear(t),this.addItems(e,t)}))}setMaxItems(e){0===e&&(e=null),this.settings.maxItems=e,this.refreshState()}setActiveItem(e,t){var i,s,n,o,r,l,a=this
if("single"!==a.settings.mode){if(!e)return a.clearActiveItems(),void(a.isFocused&&a.showInput())
if("click"===(i=t&&t.type.toLowerCase())&&K("shiftKey",t)&&a.activeItems.length){for(l=a.getLastActive(),(n=Array.prototype.indexOf.call(a.control.children,l))>(o=Array.prototype.indexOf.call(a.control.children,e))&&(r=n,n=o,o=r),s=n;s<=o;s++)e=a.control.children[s],-1===a.activeItems.indexOf(e)&&a.setActiveItemClass(e)
H(t)}else"click"===i&&K(V,t)||"keydown"===i&&K("shiftKey",t)?e.classList.contains("active")?a.removeActiveItem(e):a.setActiveItemClass(e):(a.clearActiveItems(),a.setActiveItemClass(e))
a.hideInput(),a.isFocused||a.focus()}}setActiveItemClass(e){const t=this,i=t.control.querySelector(".last-active")
i&&S(i,"last-active"),C(e,"active last-active"),t.trigger("item_select",e),-1==t.activeItems.indexOf(e)&&t.activeItems.push(e)}removeActiveItem(e){var t=this.activeItems.indexOf(e)
this.activeItems.splice(t,1),S(e,"active")}clearActiveItems(){S(this.activeItems,"active"),this.activeItems=[]}setActiveOption(e){e!==this.activeOption&&(this.clearActiveOption(),e&&(this.activeOption=e,P(this.focus_node,{"aria-activedescendant":e.getAttribute("id")}),P(e,{"aria-selected":"true"}),C(e,"active"),this.scrollToOption(e)))}scrollToOption(e,t){if(!e)return
const i=this.dropdown_content,s=i.clientHeight,n=i.scrollTop||0,o=e.offsetHeight,r=e.getBoundingClientRect().top-i.getBoundingClientRect().top+n
r+o>s+n?this.scroll(r-s+o,t):r<n&&this.scroll(r,t)}scroll(e,t){const i=this.dropdown_content
t&&(i.style.scrollBehavior=t),i.scrollTop=e,i.style.scrollBehavior=""}clearActiveOption(){this.activeOption&&(S(this.activeOption,"active"),P(this.activeOption,{"aria-selected":null})),this.activeOption=null,P(this.focus_node,{"aria-activedescendant":null})}selectAll(){if("single"===this.settings.mode)return
const e=this.controlChildren()
e.length&&(this.hideInput(),this.close(),this.activeItems=e,C(e,"active"))}inputState(){var e=this
e.control.contains(e.control_input)&&(P(e.control_input,{placeholder:e.settings.placeholder}),e.activeItems.length>0||!e.isFocused&&e.settings.hidePlaceholder&&e.items.length>0?(e.setTextboxValue(),e.isInputHidden=!0):(e.settings.hidePlaceholder&&e.items.length>0&&P(e.control_input,{placeholder:""}),e.isInputHidden=!1),e.wrapper.classList.toggle("input-hidden",e.isInputHidden))}hideInput(){this.inputState()}showInput(){this.inputState()}inputValue(){return this.control_input.value.trim()}focus(){var e=this
e.isDisabled||(e.ignoreFocus=!0,e.control_input.offsetWidth?e.control_input.focus():e.focus_node.focus(),setTimeout((()=>{e.ignoreFocus=!1,e.onFocus()}),0))}blur(){this.focus_node.blur(),this.onBlur()}getScoreFunction(e){return this.sifter.getScoreFunction(e,this.getSearchOptions())}getSearchOptions(){var e=this.settings,t=e.sortField
return"string"==typeof e.sortField&&(t=[{field:e.sortField}]),{fields:e.searchField,conjunction:e.searchConjunction,sort:t,nesting:e.nesting}}search(e){var t,i,s,n=this,o=this.getSearchOptions()
if(n.settings.score&&"function"!=typeof(s=n.settings.score.call(n,e)))throw new Error('Tom Select "score" setting must be a function that returns a function')
if(e!==n.lastQuery?(n.lastQuery=e,i=n.sifter.search(e,Object.assign(o,{score:s})),n.currentResults=i):i=Object.assign({},n.currentResults),n.settings.hideSelected)for(t=i.items.length-1;t>=0;t--){let e=q(i.items[t].id)
e&&-1!==n.items.indexOf(e)&&i.items.splice(t,1)}return i}refreshOptions(e=!0){var t,i,s,n,o,r,l,a,c,d,p
const u={},h=[]
var g,f=this,v=f.inputValue(),m=f.search(v),O=f.activeOption,b=f.settings.shouldOpen||!1,w=f.dropdown_content
for(O&&(c=O.dataset.value,d=O.closest("[data-group]")),n=m.items.length,"number"==typeof f.settings.maxOptions&&(n=Math.min(n,f.settings.maxOptions)),n>0&&(b=!0),t=0;t<n;t++){let e=m.items[t].id,n=f.options[e],l=f.getOption(e,!0)
for(f.settings.hideSelected||l.classList.toggle("selected",f.items.includes(e)),o=n[f.settings.optgroupField]||"",i=0,s=(r=Array.isArray(o)?o:[o])&&r.length;i<s;i++)o=r[i],f.optgroups.hasOwnProperty(o)||(o=""),u.hasOwnProperty(o)||(u[o]=document.createDocumentFragment(),h.push(o)),i>0&&(l=l.cloneNode(!0),P(l,{id:n.$id+"-clone-"+i,"aria-selected":null}),l.classList.add("ts-cloned"),S(l,"active")),c==e&&d&&d.dataset.group===o&&(O=l),u[o].appendChild(l)}this.settings.lockOptgroupOrder&&h.sort(((e,t)=>(f.optgroups[e]&&f.optgroups[e].$order||0)-(f.optgroups[t]&&f.optgroups[t].$order||0))),l=document.createDocumentFragment(),y(h,(e=>{if(f.optgroups.hasOwnProperty(e)&&u[e].children.length){let t=document.createDocumentFragment(),i=f.render("optgroup_header",f.optgroups[e])
G(t,i),G(t,u[e])
let s=f.render("optgroup",{group:f.optgroups[e],options:t})
G(l,s)}else G(l,u[e])})),w.innerHTML="",G(w,l),f.settings.highlight&&(g=w.querySelectorAll("span.highlight"),Array.prototype.forEach.call(g,(function(e){var t=e.parentNode
t.replaceChild(e.firstChild,e),t.normalize()})),m.query.length&&m.tokens.length&&y(m.tokens,(e=>{T(w,e.regex)})))
var _=e=>{let t=f.render(e,{input:v})
return t&&(b=!0,w.insertBefore(t,w.firstChild)),t}
if(f.loading?_("loading"):f.settings.shouldLoad.call(f,v)?0===m.items.length&&_("no_results"):_("not_loading"),(a=f.canCreate(v))&&(p=_("option_create")),f.hasOptions=m.items.length>0||a,b){if(m.items.length>0){if(!w.contains(O)&&"single"===f.settings.mode&&f.items.length&&(O=f.getOption(f.items[0])),!w.contains(O)){let e=0
p&&!f.settings.addPrecedence&&(e=1),O=f.selectable()[e]}}else p&&(O=p)
e&&!f.isOpen&&(f.open(),f.scrollToOption(O,"auto")),f.setActiveOption(O)}else f.clearActiveOption(),e&&f.isOpen&&f.close(!1)}selectable(){return this.dropdown_content.querySelectorAll("[data-selectable]")}addOption(e,t=!1){const i=this
if(Array.isArray(e))return i.addOptions(e,t),!1
const s=q(e[i.settings.valueField])
return null!==s&&!i.options.hasOwnProperty(s)&&(e.$order=e.$order||++i.order,e.$id=i.inputId+"-opt-"+e.$order,i.options[s]=e,i.lastQuery=null,t&&(i.userOptions[s]=t,i.trigger("option_add",s,e)),s)}addOptions(e,t=!1){y(e,(e=>{this.addOption(e,t)}))}registerOption(e){return this.addOption(e)}registerOptionGroup(e){var t=q(e[this.settings.optgroupValueField])
return null!==t&&(e.$order=e.$order||++this.order,this.optgroups[t]=e,t)}addOptionGroup(e,t){var i
t[this.settings.optgroupValueField]=e,(i=this.registerOptionGroup(t))&&this.trigger("optgroup_add",i,t)}removeOptionGroup(e){this.optgroups.hasOwnProperty(e)&&(delete this.optgroups[e],this.clearCache(),this.trigger("optgroup_remove",e))}clearOptionGroups(){this.optgroups={},this.clearCache(),this.trigger("optgroup_clear")}updateOption(e,t){const i=this
var s,n
const o=q(e),r=q(t[i.settings.valueField])
if(null===o)return
if(!i.options.hasOwnProperty(o))return
if("string"!=typeof r)throw new Error("Value must be set in option data")
const l=i.getOption(o),a=i.getItem(o)
if(t.$order=t.$order||i.options[o].$order,delete i.options[o],i.uncacheValue(r),i.options[r]=t,l){if(i.dropdown_content.contains(l)){const e=i._render("option",t)
E(l,e),i.activeOption===l&&i.setActiveOption(e)}l.remove()}a&&(-1!==(n=i.items.indexOf(o))&&i.items.splice(n,1,r),s=i._render("item",t),a.classList.contains("active")&&C(s,"active"),E(a,s)),i.lastQuery=null}removeOption(e,t){const i=this
e=D(e),i.uncacheValue(e),delete i.userOptions[e],delete i.options[e],i.lastQuery=null,i.trigger("option_remove",e),i.removeItem(e,t)}clearOptions(){this.loadedSearches={},this.userOptions={},this.clearCache()
var e={}
y(this.options,((t,i)=>{this.items.indexOf(i)>=0&&(e[i]=this.options[i])})),this.options=this.sifter.items=e,this.lastQuery=null,this.trigger("option_clear")}getOption(e,t=!1){const i=q(e)
if(null!==i&&this.options.hasOwnProperty(i)){const e=this.options[i]
if(e.$div)return e.$div
if(t)return this._render("option",e)}return null}getAdjacent(e,t,i="option"){var s
if(!e)return null
s="item"==i?this.controlChildren():this.dropdown_content.querySelectorAll("[data-selectable]")
for(let i=0;i<s.length;i++)if(s[i]==e)return t>0?s[i+1]:s[i-1]
return null}getItem(e){if("object"==typeof e)return e
var t=q(e)
return null!==t?this.control.querySelector(`[data-value="${Q(t)}"]`):null}addItems(e,t){var i=this,s=Array.isArray(e)?e:[e]
for(let e=0,n=(s=s.filter((e=>-1===i.items.indexOf(e)))).length;e<n;e++)i.isPending=e<n-1,i.addItem(s[e],t)}addItem(e,t){R(this,t?[]:["change"],(()=>{var i,s
const n=this,o=n.settings.mode,r=q(e)
if((!r||-1===n.items.indexOf(r)||("single"===o&&n.close(),"single"!==o&&n.settings.duplicates))&&null!==r&&n.options.hasOwnProperty(r)&&("single"===o&&n.clear(t),"multi"!==o||!n.isFull())){if(i=n._render("item",n.options[r]),n.control.contains(i)&&(i=i.cloneNode(!0)),s=n.isFull(),n.items.splice(n.caretPos,0,r),n.insertAtCaret(i),n.isSetup){if(!n.isPending&&n.settings.hideSelected){let e=n.getOption(r),t=n.getAdjacent(e,1)
t&&n.setActiveOption(t)}n.isPending||n.refreshOptions(n.isFocused&&"single"!==o),0!=n.settings.closeAfterSelect&&n.isFull()?n.close():n.isPending||n.positionDropdown(),n.trigger("item_add",r,i),n.isPending||n.updateOriginalInput({silent:t})}(!n.isPending||!s&&n.isFull())&&(n.inputState(),n.refreshState())}}))}removeItem(e=null,t){const i=this
if(!(e=i.getItem(e)))return
var s,n
const o=e.dataset.value
s=L(e),e.remove(),e.classList.contains("active")&&(n=i.activeItems.indexOf(e),i.activeItems.splice(n,1),S(e,"active")),i.items.splice(s,1),i.lastQuery=null,!i.settings.persist&&i.userOptions.hasOwnProperty(o)&&i.removeOption(o,t),s<i.caretPos&&i.setCaret(i.caretPos-1),i.updateOriginalInput({silent:t}),i.refreshState(),i.positionDropdown(),i.trigger("item_remove",o,e)}createItem(e=null,t=!0,i=(()=>{})){var s,n=this,o=n.caretPos
if(e=e||n.inputValue(),!n.canCreate(e))return i(),!1
n.lock()
var r=!1,l=e=>{if(n.unlock(),!e||"object"!=typeof e)return i()
var s=q(e[n.settings.valueField])
if("string"!=typeof s)return i()
n.setTextboxValue(),n.addOption(e,!0),n.setCaret(o),n.addItem(s),n.refreshOptions(t&&"single"!==n.settings.mode),i(e),r=!0}
return s="function"==typeof n.settings.create?n.settings.create.call(this,e,l):{[n.settings.labelField]:e,[n.settings.valueField]:e},r||l(s),!0}refreshItems(){var e=this
e.lastQuery=null,e.isSetup&&e.addItems(e.items),e.updateOriginalInput(),e.refreshState()}refreshState(){const e=this
e.refreshValidityState()
const t=e.isFull(),i=e.isLocked
e.wrapper.classList.toggle("rtl",e.rtl)
const s=e.wrapper.classList
var n
s.toggle("focus",e.isFocused),s.toggle("disabled",e.isDisabled),s.toggle("required",e.isRequired),s.toggle("invalid",!e.isValid),s.toggle("locked",i),s.toggle("full",t),s.toggle("input-active",e.isFocused&&!e.isInputHidden),s.toggle("dropdown-active",e.isOpen),s.toggle("has-options",(n=e.options,0===Object.keys(n).length)),s.toggle("has-items",e.items.length>0)}refreshValidityState(){var e=this
e.input.checkValidity&&(e.isValid=e.input.checkValidity(),e.isInvalid=!e.isValid)}isFull(){return null!==this.settings.maxItems&&this.items.length>=this.settings.maxItems}updateOriginalInput(e={}){const t=this
var i,s
const n=t.input.querySelector('option[value=""]')
if(t.is_select_tag){const e=[]
function o(i,s,o){return i||(i=w('<option value="'+N(s)+'">'+N(o)+"</option>")),i!=n&&t.input.append(i),e.push(i),i.selected=!0,i}t.input.querySelectorAll("option:checked").forEach((e=>{e.selected=!1})),0==t.items.length&&"single"==t.settings.mode?o(n,"",""):t.items.forEach((n=>{if(i=t.options[n],s=i[t.settings.labelField]||"",e.includes(i.$option)){o(t.input.querySelector(`option[value="${Q(n)}"]:not(:checked)`),n,s)}else i.$option=o(i.$option,n,s)}))}else t.input.value=t.getValue()
t.isSetup&&(e.silent||t.trigger("change",t.getValue()))}open(){var e=this
e.isLocked||e.isOpen||"multi"===e.settings.mode&&e.isFull()||(e.isOpen=!0,P(e.focus_node,{"aria-expanded":"true"}),e.refreshState(),I(e.dropdown,{visibility:"hidden",display:"block"}),e.positionDropdown(),I(e.dropdown,{visibility:"visible",display:"block"}),e.focus(),e.trigger("dropdown_open",e.dropdown))}close(e=!0){var t=this,i=t.isOpen
e&&(t.setTextboxValue(),"single"===t.settings.mode&&t.items.length&&t.hideInput()),t.isOpen=!1,P(t.focus_node,{"aria-expanded":"false"}),I(t.dropdown,{display:"none"}),t.settings.hideSelected&&t.clearActiveOption(),t.refreshState(),i&&t.trigger("dropdown_close",t.dropdown)}positionDropdown(){if("body"===this.settings.dropdownParent){var e=this.control,t=e.getBoundingClientRect(),i=e.offsetHeight+t.top+window.scrollY,s=t.left+window.scrollX
I(this.dropdown,{width:t.width+"px",top:i+"px",left:s+"px"})}}clear(e){var t=this
if(t.items.length){var i=t.controlChildren()
y(i,(e=>{t.removeItem(e,!0)})),t.showInput(),e||t.updateOriginalInput(),t.trigger("clear")}}insertAtCaret(e){const t=this,i=t.caretPos,s=t.control
s.insertBefore(e,s.children[i]),t.setCaret(i+1)}deleteSelection(e){var t,i,s,n,o,r=this
t=e&&8===e.keyCode?-1:1,i={start:(o=r.control_input).selectionStart||0,length:(o.selectionEnd||0)-(o.selectionStart||0)}
const l=[]
if(r.activeItems.length)n=F(r.activeItems,t),s=L(n),t>0&&s++,y(r.activeItems,(e=>l.push(e)))
else if((r.isFocused||"single"===r.settings.mode)&&r.items.length){const e=r.controlChildren()
t<0&&0===i.start&&0===i.length?l.push(e[r.caretPos-1]):t>0&&i.start===r.inputValue().length&&l.push(e[r.caretPos])}const a=l.map((e=>e.dataset.value))
if(!a.length||"function"==typeof r.settings.onDelete&&!1===r.settings.onDelete.call(r,a,e))return!1
for(H(e,!0),void 0!==s&&r.setCaret(s);l.length;)r.removeItem(l.pop())
return r.showInput(),r.positionDropdown(),r.refreshOptions(!1),!0}advanceSelection(e,t){var i,s,n=this
n.rtl&&(e*=-1),n.inputValue().length||(K(V,t)||K("shiftKey",t)?(s=(i=n.getLastActive(e))?i.classList.contains("active")?n.getAdjacent(i,e,"item"):i:e>0?n.control_input.nextElementSibling:n.control_input.previousElementSibling)&&(s.classList.contains("active")&&n.removeActiveItem(i),n.setActiveItemClass(s)):n.moveCaret(e))}moveCaret(e){}getLastActive(e){let t=this.control.querySelector(".last-active")
if(t)return t
var i=this.control.querySelectorAll(".active")
return i?F(i,e):void 0}setCaret(e){this.caretPos=this.items.length}controlChildren(){return Array.from(this.control.querySelectorAll("[data-ts-item]"))}lock(){this.close(),this.isLocked=!0,this.refreshState()}unlock(){this.isLocked=!1,this.refreshState()}disable(){var e=this
e.input.disabled=!0,e.control_input.disabled=!0,e.focus_node.tabIndex=-1,e.isDisabled=!0,e.lock()}enable(){var e=this
e.input.disabled=!1,e.control_input.disabled=!1,e.focus_node.tabIndex=e.tabIndex,e.isDisabled=!1,e.unlock()}destroy(){var e=this,t=e.revertSettings
e.trigger("destroy"),e.off(),e.wrapper.remove(),e.dropdown.remove(),e.input.innerHTML=t.innerHTML,e.input.tabIndex=t.tabIndex,S(e.input,"tomselected","ts-hidden-accessible"),e._destroy(),delete e.input.tomselect}render(e,t){return"function"!=typeof this.settings.render[e]?null:this._render(e,t)}_render(e,t){var i,s,n=""
const o=this
return"option"!==e&&"item"!=e||(n=D(t[o.settings.valueField])),null==(s=o.settings.render[e].call(this,t,N))||(s=w(s),"option"===e||"option_create"===e?t[o.settings.disabledField]?P(s,{"aria-disabled":"true"}):P(s,{"data-selectable":""}):"optgroup"===e&&(i=t.group[o.settings.optgroupValueField],P(s,{"data-group":i}),t.group[o.settings.disabledField]&&P(s,{"data-disabled":""})),"option"!==e&&"item"!==e||(P(s,{"data-value":n}),"item"===e?(C(s,o.settings.itemClass),P(s,{"data-ts-item":""})):(C(s,o.settings.optionClass),P(s,{role:"option",id:t.$id}),o.options[n].$div=s))),s}clearCache(){y(this.options,((e,t)=>{e.$div&&(e.$div.remove(),delete e.$div)}))}uncacheValue(e){const t=this.getOption(e)
t&&t.remove()}canCreate(e){return this.settings.create&&e.length>0&&this.settings.createFilter.call(this,e)}hook(e,t,i){var s=this,n=s[t]
s[t]=function(){var t,o
return"after"===e&&(t=n.apply(s,arguments)),o=i.apply(s,arguments),"instead"===e?o:("before"===e&&(t=n.apply(s,arguments)),t)}}}return J.define("change_listener",(function(){B(this.input,"change",(()=>{this.sync()}))})),J.define("checkbox_options",(function(){var e=this,t=e.onOptionSelect
e.settings.hideSelected=!1
var i=function(e){setTimeout((()=>{var t=e.querySelector("input")
e.classList.contains("selected")?t.checked=!0:t.checked=!1}),1)}
e.hook("after","setupTemplates",(()=>{var t=e.settings.render.option
e.settings.render.option=(i,s)=>{var n=w(t.call(e,i,s)),o=document.createElement("input")
o.addEventListener("click",(function(e){H(e)})),o.type="checkbox"
const r=q(i[e.settings.valueField])
return r&&e.items.indexOf(r)>-1&&(o.checked=!0),n.prepend(o),n}})),e.on("item_remove",(t=>{var s=e.getOption(t)
s&&(s.classList.remove("selected"),i(s))})),e.hook("instead","onOptionSelect",((s,n)=>{if(n.classList.contains("selected"))return n.classList.remove("selected"),e.removeItem(n.dataset.value),e.refreshOptions(),void H(s,!0)
t.call(e,s,n),i(n)}))})),J.define("clear_button",(function(e){const t=this,i=Object.assign({className:"clear-button",title:"Clear All",html:e=>`<div class="${e.className}" title="${e.title}">&times;</div>`},e)
t.on("initialize",(()=>{var e=w(i.html(i))
e.addEventListener("click",(e=>{t.clear(),"single"===t.settings.mode&&t.settings.allowEmptyOption&&t.addItem(""),e.preventDefault(),e.stopPropagation()})),t.control.appendChild(e)}))})),J.define("drag_drop",(function(){var e=this
if(!$.fn.sortable)throw new Error('The "drag_drop" plugin requires jQuery UI "sortable".')
if("multi"===e.settings.mode){var t=e.lock,i=e.unlock
e.hook("instead","lock",(()=>{var i=$(e.control).data("sortable")
return i&&i.disable(),t.call(e)})),e.hook("instead","unlock",(()=>{var t=$(e.control).data("sortable")
return t&&t.enable(),i.call(e)})),e.on("initialize",(()=>{var t=$(e.control).sortable({items:"[data-value]",forcePlaceholderSize:!0,disabled:e.isLocked,start:(e,i)=>{i.placeholder.css("width",i.helper.css("width")),t.css({overflow:"visible"})},stop:()=>{t.css({overflow:"hidden"})
var i=[]
t.children("[data-value]").each((function(){this.dataset.value&&i.push(this.dataset.value)})),e.setValue(i)}})}))}})),J.define("dropdown_header",(function(e){const t=this,i=Object.assign({title:"Untitled",headerClass:"dropdown-header",titleRowClass:"dropdown-header-title",labelClass:"dropdown-header-label",closeClass:"dropdown-header-close",html:e=>'<div class="'+e.headerClass+'"><div class="'+e.titleRowClass+'"><span class="'+e.labelClass+'">'+e.title+'</span><a class="'+e.closeClass+'">&times;</a></div></div>'},e)
t.on("initialize",(()=>{var e=w(i.html(i)),s=e.querySelector("."+i.closeClass)
s&&s.addEventListener("click",(e=>{H(e,!0),t.close()})),t.dropdown.insertBefore(e,t.dropdown.firstChild)}))})),J.define("caret_position",(function(){var e=this
e.hook("instead","setCaret",(t=>{"single"!==e.settings.mode&&e.control.contains(e.control_input)?(t=Math.max(0,Math.min(e.items.length,t)))==e.caretPos||e.isPending||e.controlChildren().forEach(((i,s)=>{s<t?e.control_input.insertAdjacentElement("beforebegin",i):e.control.appendChild(i)})):t=e.items.length,e.caretPos=t})),e.hook("instead","moveCaret",(t=>{if(!e.isFocused)return
const i=e.getLastActive(t)
if(i){const s=L(i)
e.setCaret(t>0?s+1:s),e.setActiveItem()}else e.setCaret(e.caretPos+t)}))})),J.define("dropdown_input",(function(){var e=this
e.settings.shouldOpen=!0,e.hook("before","setup",(()=>{e.focus_node=e.control,C(e.control_input,"dropdown-input")
const t=w('<div class="dropdown-input-wrap">')
t.append(e.control_input),e.dropdown.insertBefore(t,e.dropdown.firstChild)})),e.on("initialize",(()=>{e.control_input.addEventListener("keydown",(t=>{switch(t.keyCode){case 27:return e.isOpen&&(H(t,!0),e.close()),void e.clearActiveItems()
case 9:e.focus_node.tabIndex=-1}return e.onKeyDown.call(e,t)})),e.on("blur",(()=>{e.focus_node.tabIndex=e.isDisabled?-1:e.tabIndex})),e.on("dropdown_open",(()=>{e.control_input.focus()}))
const t=e.onBlur
e.hook("instead","onBlur",(i=>{if(!i||i.relatedTarget!=e.control_input)return t.call(e)})),B(e.control_input,"blur",(()=>e.onBlur())),e.hook("before","close",(()=>{e.isOpen&&e.focus_node.focus()}))}))})),J.define("input_autogrow",(function(){var e=this
e.on("initialize",(()=>{var t=document.createElement("span"),i=e.control_input
t.style.cssText="position:absolute; top:-99999px; left:-99999px; width:auto; padding:0; white-space:pre; ",e.wrapper.appendChild(t)
for(const e of["letterSpacing","fontSize","fontFamily","fontWeight","textTransform"])t.style[e]=i.style[e]
var s=()=>{e.items.length>0?(t.textContent=i.value,i.style.width=t.clientWidth+"px"):i.style.width=""}
s(),e.on("update item_add item_remove",s),B(i,"input",s),B(i,"keyup",s),B(i,"blur",s),B(i,"update",s)}))})),J.define("no_backspace_delete",(function(){var e=this,t=e.deleteSelection
this.hook("instead","deleteSelection",(i=>!!e.activeItems.length&&t.call(e,i)))})),J.define("no_active_items",(function(){this.hook("instead","setActiveItem",(()=>{})),this.hook("instead","selectAll",(()=>{}))})),J.define("optgroup_columns",(function(){var e=this,t=e.onKeyDown
e.hook("instead","onKeyDown",(i=>{var s,n,o,r
if(!e.isOpen||37!==i.keyCode&&39!==i.keyCode)return t.call(e,i)
r=k(e.activeOption,"[data-group]"),s=L(e.activeOption,"[data-selectable]"),r&&(r=37===i.keyCode?r.previousSibling:r.nextSibling)&&(n=(o=r.querySelectorAll("[data-selectable]"))[Math.min(o.length-1,s)])&&e.setActiveOption(n)}))})),J.define("remove_button",(function(e){const t=Object.assign({label:"&times;",title:"Remove",className:"remove",append:!0},e)
var i=this
if(t.append){var s='<a href="javascript:void(0)" class="'+t.className+'" tabindex="-1" title="'+N(t.title)+'">'+t.label+"</a>"
i.hook("after","setupTemplates",(()=>{var e=i.settings.render.item
i.settings.render.item=(t,n)=>{var o=w(e.call(i,t,n)),r=w(s)
return o.appendChild(r),B(r,"mousedown",(e=>{H(e,!0)})),B(r,"click",(e=>{if(H(e,!0),!i.isLocked){var t=o.dataset.value
i.removeItem(t),i.refreshOptions(!1)}})),o}}))}})),J.define("restore_on_backspace",(function(e){const t=this,i=Object.assign({text:e=>e[t.settings.labelField]},e)
t.on("item_remove",(function(e){if(""===t.control_input.value.trim()){var s=t.options[e]
s&&t.setTextboxValue(i.text.call(t,s))}}))})),J.define("virtual_scroll",(function(){const e=this,t=e.canLoad,i=e.clearActiveOption,s=e.loadCallback
var n,o={},r=!1
if(!e.settings.firstUrl)throw"virtual_scroll plugin requires a firstUrl() method"
function l(t){return!("number"==typeof e.settings.maxOptions&&n.children.length>=e.settings.maxOptions)&&!(!(t in o)||!o[t])}e.settings.sortField=[{field:"$order"},{field:"$score"}],e.setNextUrl=function(e,t){o[e]=t},e.getUrl=function(t){if(t in o){const e=o[t]
return o[t]=!1,e}return o={},e.settings.firstUrl(t)},e.hook("instead","clearActiveOption",(()=>{if(!r)return i.call(e)})),e.hook("instead","canLoad",(i=>i in o?l(i):t.call(e,i))),e.hook("instead","loadCallback",((t,i)=>{r||e.clearOptions(),s.call(e,t,i),r=!1})),e.hook("after","refreshOptions",(()=>{const t=e.lastValue
var i
l(t)?(i=e.render("loading_more",{query:t}))&&i.setAttribute("data-selectable",""):t in o&&!n.querySelector(".no-results")&&(i=e.render("no_more_results",{query:t})),i&&(C(i,e.settings.optionClass),n.append(i))})),e.on("initialize",(()=>{n=e.dropdown_content,e.settings.render=Object.assign({},{loading_more:function(){return'<div class="loading-more-results">Loading more results ... </div>'},no_more_results:function(){return'<div class="no-more-results">No more results</div>'}},e.settings.render),n.addEventListener("scroll",(function(){n.clientHeight/(n.scrollHeight-n.scrollTop)<.95||l(e.lastValue)&&(r||(r=!0,e.load.call(e,e.lastValue)))}))}))})),J}))
var tomSelect=function(e,t){return new TomSelect(e,t)}
//# sourceMappingURL=tom-select.complete.min.js.map

View file

@ -1,334 +0,0 @@
/**
* tom-select.css (v2.0.0-rc.4)
* Copyright (c) contributors
*
* 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.
*
*/
.ts-wrapper.plugin-drag_drop.multi > .ts-control > div.ui-sortable-placeholder {
visibility: visible !important;
background: #f2f2f2 !important;
background: rgba(0, 0, 0, 0.06) !important;
border: 0 none !important;
box-shadow: inset 0 0 12px 4px #fff; }
.ts-wrapper.plugin-drag_drop .ui-sortable-placeholder::after {
content: '!';
visibility: hidden; }
.ts-wrapper.plugin-drag_drop .ui-sortable-helper {
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); }
.plugin-checkbox_options .option input {
margin-right: 0.5rem; }
.plugin-clear_button .ts-control {
padding-right: calc( 1em + (3 * 6px)) !important; }
.plugin-clear_button .clear-button {
opacity: 0;
position: absolute;
top: 8px;
right: calc(8px - 6px);
margin-right: 0 !important;
background: transparent !important;
transition: opacity 0.5s;
cursor: pointer; }
.plugin-clear_button.single .clear-button {
right: calc(8px - 6px + 2rem); }
.plugin-clear_button.focus.has-items .clear-button,
.plugin-clear_button:hover.has-items .clear-button {
opacity: 1; }
.ts-wrapper .dropdown-header {
position: relative;
padding: 10px 8px;
border-bottom: 1px solid #d0d0d0;
background: #f8f8f8;
border-radius: 3px 3px 0 0; }
.ts-wrapper .dropdown-header-close {
position: absolute;
right: 8px;
top: 50%;
color: #303030;
opacity: 0.4;
margin-top: -12px;
line-height: 20px;
font-size: 20px !important; }
.ts-wrapper .dropdown-header-close:hover {
color: black; }
.plugin-dropdown_input.focus.dropdown-active .ts-control {
box-shadow: none;
border: 1px solid #d0d0d0; }
.plugin-dropdown_input .dropdown-input {
border: 1px solid #d0d0d0;
border-width: 0 0 1px 0;
display: block;
padding: 8px 8px;
box-shadow: none;
width: 100%;
background: transparent; }
.ts-wrapper.plugin-input_autogrow.has-items .ts-control > input {
min-width: 0; }
.ts-wrapper.plugin-input_autogrow.has-items.focus .ts-control > input {
flex: none;
min-width: 4px; }
.ts-wrapper.plugin-input_autogrow.has-items.focus .ts-control > input::-webkit-input-placeholder {
color: transparent; }
.ts-wrapper.plugin-input_autogrow.has-items.focus .ts-control > input::-ms-input-placeholder {
color: transparent; }
.ts-wrapper.plugin-input_autogrow.has-items.focus .ts-control > input::placeholder {
color: transparent; }
.ts-dropdown.plugin-optgroup_columns .ts-dropdown-content {
display: flex; }
.ts-dropdown.plugin-optgroup_columns .optgroup {
border-right: 1px solid #f2f2f2;
border-top: 0 none;
flex-grow: 1;
flex-basis: 0;
min-width: 0; }
.ts-dropdown.plugin-optgroup_columns .optgroup:last-child {
border-right: 0 none; }
.ts-dropdown.plugin-optgroup_columns .optgroup:before {
display: none; }
.ts-dropdown.plugin-optgroup_columns .optgroup-header {
border-top: 0 none; }
.ts-wrapper.plugin-remove_button .item {
display: inline-flex;
align-items: center;
padding-right: 0 !important; }
.ts-wrapper.plugin-remove_button .item .remove {
color: inherit;
text-decoration: none;
vertical-align: middle;
display: inline-block;
padding: 2px 6px;
border-left: 1px solid #d0d0d0;
border-radius: 0 2px 2px 0;
box-sizing: border-box;
margin-left: 6px; }
.ts-wrapper.plugin-remove_button .item .remove:hover {
background: rgba(0, 0, 0, 0.05); }
.ts-wrapper.plugin-remove_button .item.active .remove {
border-left-color: #cacaca; }
.ts-wrapper.plugin-remove_button.disabled .item .remove:hover {
background: none; }
.ts-wrapper.plugin-remove_button.disabled .item .remove {
border-left-color: white; }
.ts-wrapper.plugin-remove_button .remove-single {
position: absolute;
right: 0;
top: 0;
font-size: 23px; }
.ts-wrapper {
position: relative; }
.ts-dropdown,
.ts-control,
.ts-control input {
color: #303030;
font-family: inherit;
font-size: 13px;
line-height: 18px;
font-smoothing: inherit; }
.ts-control,
.ts-wrapper.single.input-active .ts-control {
background: #fff;
cursor: text; }
.ts-control {
border: 1px solid #d0d0d0;
padding: 8px 8px;
width: 100%;
overflow: hidden;
position: relative;
z-index: 1;
box-sizing: border-box;
box-shadow: none;
border-radius: 3px;
display: flex;
flex-wrap: wrap; }
.ts-wrapper.multi.has-items .ts-control {
padding: calc( 8px - 2px - 0) 8px calc( 8px - 2px - 3px - 0); }
.full .ts-control {
background-color: #fff; }
.disabled .ts-control,
.disabled .ts-control * {
cursor: default !important; }
.focus .ts-control {
box-shadow: none; }
.ts-control > * {
vertical-align: baseline;
display: inline-block; }
.ts-wrapper.multi .ts-control > div {
cursor: pointer;
margin: 0 3px 3px 0;
padding: 2px 6px;
background: #f2f2f2;
color: #303030;
border: 0 solid #d0d0d0; }
.ts-wrapper.multi .ts-control > div.active {
background: #e8e8e8;
color: #303030;
border: 0 solid #cacaca; }
.ts-wrapper.multi.disabled .ts-control > div, .ts-wrapper.multi.disabled .ts-control > div.active {
color: #7d7c7c;
background: white;
border: 0 solid white; }
.ts-control > input {
flex: 1 1 auto;
min-width: 7rem;
display: inline-block !important;
padding: 0 !important;
min-height: 0 !important;
max-height: none !important;
max-width: 100% !important;
margin: 0 !important;
text-indent: 0 !important;
border: 0 none !important;
background: none !important;
line-height: inherit !important;
-webkit-user-select: auto !important;
-moz-user-select: auto !important;
-ms-user-select: auto !important;
user-select: auto !important;
box-shadow: none !important; }
.ts-control > input::-ms-clear {
display: none; }
.ts-control > input:focus {
outline: none !important; }
.has-items .ts-control > input {
margin: 0 4px !important; }
.ts-control.rtl {
text-align: right; }
.ts-control.rtl.single .ts-control:after {
left: 15px;
right: auto; }
.ts-control.rtl .ts-control > input {
margin: 0 4px 0 -2px !important; }
.disabled .ts-control {
opacity: 0.5;
background-color: #fafafa; }
.input-hidden .ts-control > input {
opacity: 0;
position: absolute;
left: -10000px; }
.ts-dropdown {
position: absolute;
top: 100%;
left: 0;
width: 100%;
z-index: 10;
border: 1px solid #d0d0d0;
background: #fff;
margin: 0.25rem 0 0 0;
border-top: 0 none;
box-sizing: border-box;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
border-radius: 0 0 3px 3px; }
.ts-dropdown [data-selectable] {
cursor: pointer;
overflow: hidden; }
.ts-dropdown [data-selectable] .highlight {
background: rgba(125, 168, 208, 0.2);
border-radius: 1px; }
.ts-dropdown .option,
.ts-dropdown .optgroup-header,
.ts-dropdown .no-results,
.ts-dropdown .create {
padding: 5px 8px; }
.ts-dropdown .option, .ts-dropdown [data-disabled], .ts-dropdown [data-disabled] [data-selectable].option {
cursor: inherit;
opacity: 0.5; }
.ts-dropdown [data-selectable].option {
opacity: 1;
cursor: pointer; }
.ts-dropdown .optgroup:first-child .optgroup-header {
border-top: 0 none; }
.ts-dropdown .optgroup-header {
color: #303030;
background: #fff;
cursor: default; }
.ts-dropdown .create:hover,
.ts-dropdown .option:hover,
.ts-dropdown .active {
background-color: #f5fafd;
color: #495c68; }
.ts-dropdown .create:hover.create,
.ts-dropdown .option:hover.create,
.ts-dropdown .active.create {
color: #495c68; }
.ts-dropdown .create {
color: rgba(48, 48, 48, 0.5); }
.ts-dropdown .spinner {
display: inline-block;
width: 30px;
height: 30px;
margin: 5px 8px; }
.ts-dropdown .spinner:after {
content: " ";
display: block;
width: 24px;
height: 24px;
margin: 3px;
border-radius: 50%;
border: 5px solid #d0d0d0;
border-color: #d0d0d0 transparent #d0d0d0 transparent;
animation: lds-dual-ring 1.2s linear infinite; }
@keyframes lds-dual-ring {
0% {
transform: rotate(0deg); }
100% {
transform: rotate(360deg); } }
.ts-dropdown-content {
overflow-y: auto;
overflow-x: hidden;
max-height: 200px;
overflow-scrolling: touch;
scroll-behavior: smooth; }
.ts-hidden-accessible {
border: 0 !important;
clip: rect(0 0 0 0) !important;
-webkit-clip-path: inset(50%) !important;
clip-path: inset(50%) !important;
height: 1px !important;
overflow: hidden !important;
padding: 0 !important;
position: absolute !important;
width: 1px !important;
white-space: nowrap !important; }
/*# sourceMappingURL=tom-select.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -4,7 +4,32 @@ Memory System for AI Agents.
Temporal + Semantic Memory Architecture using PostgreSQL with pgvector.
"""
from .temporal_semantic_memory import TemporalSemanticMemory
from .visualizer import MemoryVisualizer, LiveSearchTracer
from .visualizer import MemoryVisualizer
from .search_trace import (
SearchTrace,
QueryInfo,
EntryPoint,
NodeVisit,
WeightComponents,
LinkInfo,
PruningDecision,
SearchSummary,
SearchPhaseMetrics,
)
from .search_tracer import SearchTracer
__all__ = ["TemporalSemanticMemory", "MemoryVisualizer", "LiveSearchTracer"]
__all__ = [
"TemporalSemanticMemory",
"MemoryVisualizer",
"SearchTrace",
"SearchTracer",
"QueryInfo",
"EntryPoint",
"NodeVisit",
"WeightComponents",
"LinkInfo",
"PruningDecision",
"SearchSummary",
"SearchPhaseMetrics",
]
__version__ = "0.1.0"

166
memory/search_trace.py Normal file
View file

@ -0,0 +1,166 @@
"""
Search trace models for debugging and visualization.
These Pydantic models define the structure of search traces, capturing
every step of the spreading activation search process for analysis.
"""
from datetime import datetime
from typing import List, Optional, Dict, Any, Literal
from pydantic import BaseModel, Field
class QueryInfo(BaseModel):
"""Information about the search query."""
query_text: str = Field(description="Original query text")
query_embedding: List[float] = Field(description="Generated query embedding vector")
timestamp: datetime = Field(description="When the query was executed")
thinking_budget: int = Field(description="Maximum nodes to explore")
top_k: int = Field(description="Number of results requested")
class EntryPoint(BaseModel):
"""An entry point node selected for search."""
node_id: str = Field(description="Memory unit ID")
text: str = Field(description="Memory unit text content")
similarity_score: float = Field(description="Cosine similarity to query", ge=0.0, le=1.0)
rank: int = Field(description="Rank among entry points (1-based)")
class WeightComponents(BaseModel):
"""Breakdown of weight calculation components."""
activation: float = Field(description="Activation from spreading", ge=0.0, le=1.0)
semantic_similarity: float = Field(description="Semantic similarity to query", ge=0.0, le=1.0)
recency: float = Field(description="Recency weight", ge=0.0, le=1.0)
frequency: float = Field(description="Normalized frequency weight", ge=0.0, le=1.0)
final_weight: float = Field(description="Combined final weight")
# Weight formula components (for transparency)
activation_contribution: float = Field(description="0.3 * activation")
semantic_contribution: float = Field(description="0.3 * semantic_similarity")
recency_contribution: float = Field(description="0.25 * recency")
frequency_contribution: float = Field(description="0.15 * frequency")
class LinkInfo(BaseModel):
"""Information about a link to a neighbor."""
to_node_id: str = Field(description="Target node ID")
link_type: Literal["temporal", "semantic", "entity"] = Field(description="Type of link")
link_weight: float = Field(description="Weight of the link", ge=0.0, le=1.0)
entity_id: Optional[str] = Field(default=None, description="Entity ID if link_type is 'entity'")
new_activation: float = Field(description="Activation that would be passed to neighbor")
followed: bool = Field(description="Whether this link was followed (or pruned)")
prune_reason: Optional[str] = Field(default=None, description="Why link was not followed (if not followed)")
class NodeVisit(BaseModel):
"""Information about visiting a node during search."""
step: int = Field(description="Step number in search (1-based)")
node_id: str = Field(description="Memory unit ID")
text: str = Field(description="Memory unit text content")
context: str = Field(description="Memory unit context")
event_date: datetime = Field(description="When the memory occurred")
access_count: int = Field(description="Number of times accessed before this search")
# How this node was reached
is_entry_point: bool = Field(description="Whether this is an entry point")
parent_node_id: Optional[str] = Field(default=None, description="Node that led to this one")
link_type: Optional[Literal["temporal", "semantic", "entity"]] = Field(default=None, description="Type of link from parent")
link_weight: Optional[float] = Field(default=None, description="Weight of link from parent")
# Weights
weights: WeightComponents = Field(description="Weight calculation breakdown")
# Neighbors discovered from this node
neighbors_explored: List[LinkInfo] = Field(default_factory=list, description="Links explored from this node")
# Ranking
final_rank: Optional[int] = Field(default=None, description="Final rank in results (1-based, None if not in top-k)")
class PruningDecision(BaseModel):
"""Records when a node was considered but not visited."""
node_id: str = Field(description="Node that was pruned")
reason: Literal["already_visited", "activation_too_low", "budget_exhausted"] = Field(description="Why it was pruned")
activation: float = Field(description="Activation value when pruned")
would_have_been_step: int = Field(description="What step it would have been if visited")
class SearchPhaseMetrics(BaseModel):
"""Performance metrics for a search phase."""
phase_name: str = Field(description="Name of the phase")
duration_seconds: float = Field(description="Time taken in seconds")
details: Dict[str, Any] = Field(default_factory=dict, description="Additional phase-specific metrics")
class SearchSummary(BaseModel):
"""Summary statistics about the search."""
total_nodes_visited: int = Field(description="Total nodes visited")
total_nodes_pruned: int = Field(description="Total nodes pruned")
entry_points_found: int = Field(description="Number of entry points")
budget_used: int = Field(description="How much budget was used")
budget_remaining: int = Field(description="How much budget remained")
total_duration_seconds: float = Field(description="Total search duration")
results_returned: int = Field(description="Number of results returned")
# Link statistics
temporal_links_followed: int = Field(default=0, description="Temporal links followed")
semantic_links_followed: int = Field(default=0, description="Semantic links followed")
entity_links_followed: int = Field(default=0, description="Entity links followed")
# Phase timings
phase_metrics: List[SearchPhaseMetrics] = Field(default_factory=list, description="Metrics for each phase")
class SearchTrace(BaseModel):
"""Complete trace of a search operation."""
query: QueryInfo = Field(description="Query information")
entry_points: List[EntryPoint] = Field(description="Entry points selected for search")
visits: List[NodeVisit] = Field(description="All nodes visited during search (in order)")
pruned: List[PruningDecision] = Field(default_factory=list, description="Nodes that were pruned")
summary: SearchSummary = Field(description="Summary statistics")
# Final results (for comparison with visits)
final_results: List[Dict[str, Any]] = Field(description="Final ranked results returned to user")
model_config = {
"json_encoders": {
datetime: lambda v: v.isoformat()
}
}
def to_json(self, **kwargs) -> str:
"""Export trace as JSON string."""
return self.model_dump_json(indent=2, **kwargs)
def to_dict(self) -> dict:
"""Export trace as dictionary."""
return self.model_dump()
def get_visit_by_node_id(self, node_id: str) -> Optional[NodeVisit]:
"""Find a visit by node ID."""
for visit in self.visits:
if visit.node_id == node_id:
return visit
return None
def get_search_path_to_node(self, node_id: str) -> List[NodeVisit]:
"""Get the path from entry point to a specific node."""
path = []
current_visit = self.get_visit_by_node_id(node_id)
while current_visit:
path.insert(0, current_visit)
if current_visit.parent_node_id:
current_visit = self.get_visit_by_node_id(current_visit.parent_node_id)
else:
break
return path
def get_nodes_by_link_type(self, link_type: Literal["temporal", "semantic", "entity"]) -> List[NodeVisit]:
"""Get all nodes reached via a specific link type."""
return [v for v in self.visits if v.link_type == link_type]
def get_entry_point_nodes(self) -> List[NodeVisit]:
"""Get all entry point visits."""
return [v for v in self.visits if v.is_entry_point]

322
memory/search_tracer.py Normal file
View file

@ -0,0 +1,322 @@
"""
Search tracer for collecting detailed search execution traces.
The SearchTracer collects comprehensive information about each step
of the spreading activation search process for debugging and visualization.
"""
import time
from datetime import datetime, timezone
from typing import List, Optional, Dict, Any, Literal
from .search_trace import (
SearchTrace,
QueryInfo,
EntryPoint,
NodeVisit,
WeightComponents,
LinkInfo,
PruningDecision,
SearchSummary,
SearchPhaseMetrics,
)
class SearchTracer:
"""
Tracer for collecting detailed search execution information.
Usage:
tracer = SearchTracer(query="Who is Alice?", thinking_budget=50, top_k=10)
tracer.start()
# During search...
tracer.record_query_embedding(embedding)
tracer.add_entry_point(node_id, text, similarity, rank)
tracer.visit_node(...)
tracer.prune_node(...)
# After search...
trace = tracer.finalize(final_results)
json_output = trace.to_json()
"""
def __init__(self, query: str, thinking_budget: int, top_k: int):
"""
Initialize tracer.
Args:
query: Search query text
thinking_budget: Maximum nodes to explore
top_k: Number of results requested
"""
self.query_text = query
self.thinking_budget = thinking_budget
self.top_k = top_k
# Trace data
self.query_embedding: Optional[List[float]] = None
self.start_time: Optional[float] = None
self.entry_points: List[EntryPoint] = []
self.visits: List[NodeVisit] = []
self.pruned: List[PruningDecision] = []
self.phase_metrics: List[SearchPhaseMetrics] = []
# Tracking state
self.current_step = 0
self.nodes_visited_set = set() # For quick lookups
# Link statistics
self.temporal_links_followed = 0
self.semantic_links_followed = 0
self.entity_links_followed = 0
def start(self):
"""Start timing the search."""
self.start_time = time.time()
def record_query_embedding(self, embedding: List[float]):
"""Record the query embedding."""
self.query_embedding = embedding
def add_entry_point(self, node_id: str, text: str, similarity: float, rank: int):
"""
Record an entry point.
Args:
node_id: Memory unit ID
text: Memory unit text
similarity: Cosine similarity to query
rank: Rank among entry points (1-based)
"""
self.entry_points.append(
EntryPoint(
node_id=node_id,
text=text,
similarity_score=similarity,
rank=rank,
)
)
def visit_node(
self,
node_id: str,
text: str,
context: str,
event_date: datetime,
access_count: int,
is_entry_point: bool,
parent_node_id: Optional[str],
link_type: Optional[Literal["temporal", "semantic", "entity"]],
link_weight: Optional[float],
activation: float,
semantic_similarity: float,
recency: float,
frequency: float,
final_weight: float,
):
"""
Record visiting a node.
Args:
node_id: Memory unit ID
text: Memory unit text
context: Memory unit context
event_date: When the memory occurred
access_count: Access count before this search
is_entry_point: Whether this is an entry point
parent_node_id: Node that led here (None for entry points)
link_type: Type of link from parent
link_weight: Weight of link from parent
activation: Activation score
semantic_similarity: Semantic similarity to query
recency: Recency weight
frequency: Frequency weight
final_weight: Combined final weight
"""
self.current_step += 1
self.nodes_visited_set.add(node_id)
# Calculate weight contributions for transparency
weights = WeightComponents(
activation=activation,
semantic_similarity=semantic_similarity,
recency=recency,
frequency=frequency,
final_weight=final_weight,
activation_contribution=0.3 * activation,
semantic_contribution=0.3 * semantic_similarity,
recency_contribution=0.25 * recency,
frequency_contribution=0.15 * frequency,
)
visit = NodeVisit(
step=self.current_step,
node_id=node_id,
text=text,
context=context,
event_date=event_date,
access_count=access_count,
is_entry_point=is_entry_point,
parent_node_id=parent_node_id,
link_type=link_type,
link_weight=link_weight,
weights=weights,
neighbors_explored=[],
final_rank=None, # Will be set later
)
self.visits.append(visit)
# Track link statistics
if link_type == "temporal":
self.temporal_links_followed += 1
elif link_type == "semantic":
self.semantic_links_followed += 1
elif link_type == "entity":
self.entity_links_followed += 1
def add_neighbor_link(
self,
from_node_id: str,
to_node_id: str,
link_type: Literal["temporal", "semantic", "entity"],
link_weight: float,
entity_id: Optional[str],
new_activation: float,
followed: bool,
prune_reason: Optional[str] = None,
):
"""
Record a link to a neighbor (whether followed or not).
Args:
from_node_id: Source node
to_node_id: Target node
link_type: Type of link
link_weight: Weight of link
entity_id: Entity ID if link is entity-based
new_activation: Activation passed to neighbor
followed: Whether link was followed
prune_reason: Why link was not followed (if not followed)
"""
# Find the visit for the source node
visit = None
for v in self.visits:
if v.node_id == from_node_id:
visit = v
break
if visit is None:
# Node not found, skip
return
link_info = LinkInfo(
to_node_id=to_node_id,
link_type=link_type,
link_weight=link_weight,
entity_id=entity_id,
new_activation=new_activation,
followed=followed,
prune_reason=prune_reason,
)
visit.neighbors_explored.append(link_info)
def prune_node(
self,
node_id: str,
reason: Literal["already_visited", "activation_too_low", "budget_exhausted"],
activation: float,
):
"""
Record a node being pruned (not visited).
Args:
node_id: Node that was pruned
reason: Why it was pruned
activation: Activation value when pruned
"""
self.pruned.append(
PruningDecision(
node_id=node_id,
reason=reason,
activation=activation,
would_have_been_step=self.current_step + 1,
)
)
def add_phase_metric(self, phase_name: str, duration_seconds: float, details: Optional[Dict[str, Any]] = None):
"""
Record metrics for a search phase.
Args:
phase_name: Name of the phase
duration_seconds: Time taken
details: Additional phase-specific details
"""
self.phase_metrics.append(
SearchPhaseMetrics(
phase_name=phase_name,
duration_seconds=duration_seconds,
details=details or {},
)
)
def finalize(self, final_results: List[Dict[str, Any]]) -> SearchTrace:
"""
Finalize the trace and return the complete SearchTrace object.
Args:
final_results: Final ranked results returned to user
Returns:
Complete SearchTrace object
"""
if self.start_time is None:
raise ValueError("Tracer not started - call start() first")
total_duration = time.time() - self.start_time
# Set final ranks on visits based on results
for rank, result in enumerate(final_results, 1):
result_node_id = result["id"]
for visit in self.visits:
if visit.node_id == result_node_id:
visit.final_rank = rank
break
# Create query info
query_info = QueryInfo(
query_text=self.query_text,
query_embedding=self.query_embedding or [],
timestamp=datetime.now(timezone.utc),
thinking_budget=self.thinking_budget,
top_k=self.top_k,
)
# Create summary
summary = SearchSummary(
total_nodes_visited=len(self.visits),
total_nodes_pruned=len(self.pruned),
entry_points_found=len(self.entry_points),
budget_used=len(self.visits),
budget_remaining=self.thinking_budget - len(self.visits),
total_duration_seconds=total_duration,
results_returned=len(final_results),
temporal_links_followed=self.temporal_links_followed,
semantic_links_followed=self.semantic_links_followed,
entity_links_followed=self.entity_links_followed,
phase_metrics=self.phase_metrics,
)
# Create complete trace
trace = SearchTrace(
query=query_info,
entry_points=self.entry_points,
visits=self.visits,
pruned=self.pruned,
summary=summary,
final_results=final_results,
)
return trace

View file

@ -110,6 +110,53 @@ class TemporalSemanticMemory:
self.embedding_model = SentenceTransformer(embedding_model)
print(f"✓ Model loaded (embedding dim: {self.embedding_model.get_sentence_embedding_dimension()})")
# Background queue for access count updates (to avoid blocking searches)
self._access_count_queue = asyncio.Queue()
self._access_count_worker_task = None
self._shutdown_event = asyncio.Event()
async def _access_count_worker(self):
"""Background worker that processes access count updates in batches."""
pool = self._pool # Pool is guaranteed to exist when worker starts
while not self._shutdown_event.is_set():
try:
# Collect updates for up to 1 second or 1000 items
updates = {}
deadline = asyncio.get_event_loop().time() + 1.0
while len(updates) < 1000 and asyncio.get_event_loop().time() < deadline:
try:
# Wait for items with short timeout
remaining_time = max(0.1, deadline - asyncio.get_event_loop().time())
node_ids = await asyncio.wait_for(
self._access_count_queue.get(),
timeout=remaining_time
)
# Deduplicate by adding to set
for node_id in node_ids:
updates[node_id] = True
except asyncio.TimeoutError:
break
# Process batch if we have updates
if updates:
node_id_list = list(updates.keys())
try:
async with pool.acquire() as conn:
await conn.execute(
"UPDATE memory_units SET access_count = access_count + 1 WHERE id::text = ANY($1)",
node_id_list
)
except Exception as e:
print(f"[ACCESS_COUNT_WORKER] Error updating access counts: {e}")
except asyncio.CancelledError:
break
except Exception as e:
print(f"[ACCESS_COUNT_WORKER] Unexpected error: {e}")
await asyncio.sleep(1) # Backoff on error
async def _get_pool(self) -> asyncpg.Pool:
"""Get or create the connection pool (lazy initialization)."""
if self._pool is None:
@ -125,10 +172,27 @@ class TemporalSemanticMemory:
# Initialize entity resolver with pool
if self.entity_resolver is None:
self.entity_resolver = EntityResolver(self._pool)
# Start access count worker (outside lock, after pool is created)
if self._access_count_worker_task is None and self._pool is not None:
self._access_count_worker_task = asyncio.create_task(self._access_count_worker())
return self._pool
async def close(self):
"""Close the connection pool."""
"""Close the connection pool and shutdown background workers."""
# Signal shutdown to worker
self._shutdown_event.set()
# Cancel and wait for worker task
if self._access_count_worker_task is not None:
self._access_count_worker_task.cancel()
try:
await self._access_count_worker_task
except asyncio.CancelledError:
pass
# Close pool
if self._pool is not None:
await self._pool.close()
self._pool = None
@ -494,8 +558,12 @@ class TemporalSemanticMemory:
query: str,
thinking_budget: int = 50,
top_k: int = 10,
live_tracer=None,
) -> List[Dict[str, Any]]:
enable_trace: bool = False,
weight_activation: float = 0.30,
weight_semantic: float = 0.30,
weight_recency: float = 0.25,
weight_frequency: float = 0.15,
) -> tuple[List[Dict[str, Any]], Optional[Any]]:
"""
Search memories using spreading activation (synchronous wrapper).
@ -507,13 +575,20 @@ class TemporalSemanticMemory:
query: Search query
thinking_budget: How many units to explore (computational budget)
top_k: Number of results to return
live_tracer: Optional LiveSearchTracer for visualization
enable_trace: If True, returns detailed SearchTrace object
weight_activation: Weight for activation component (default: 0.30)
weight_semantic: Weight for semantic similarity component (default: 0.30)
weight_recency: Weight for recency component (default: 0.25)
weight_frequency: Weight for frequency component (default: 0.15)
Returns:
List of memory units with their weights, sorted by relevance
Tuple of (results, trace)
"""
# Run async version synchronously
return asyncio.run(self.search_async(agent_id, query, thinking_budget, top_k, live_tracer))
return asyncio.run(self.search_async(
agent_id, query, thinking_budget, top_k, enable_trace,
weight_activation, weight_semantic, weight_recency, weight_frequency
))
async def search_async(
self,
@ -521,8 +596,12 @@ class TemporalSemanticMemory:
query: str,
thinking_budget: int = 50,
top_k: int = 10,
live_tracer=None,
) -> List[Dict[str, Any]]:
enable_trace: bool = False,
weight_activation: float = 0.30,
weight_semantic: float = 0.30,
weight_recency: float = 0.25,
weight_frequency: float = 0.15,
) -> tuple[List[Dict[str, Any]], Optional[Any]]:
"""
Search memories using spreading activation (ASYNC version).
@ -542,24 +621,41 @@ class TemporalSemanticMemory:
Returns:
List of memory units with their weights, sorted by relevance
"""
# Initialize tracer if requested
from .search_tracer import SearchTracer
tracer = SearchTracer(query, thinking_budget, top_k) if enable_trace else None
if tracer:
tracer.start()
pool = await self._get_pool()
async with pool.acquire() as conn:
search_start = time.time()
print(f"\n[SEARCH] Starting search for query: '{query[:50]}...' (thinking_budget={thinking_budget}, top_k={top_k})")
search_start = time.time()
print(f"\n[SEARCH] Starting search for query: '{query[:50]}...' (thinking_budget={thinking_budget}, top_k={top_k})")
try:
# Step 1: Generate query embedding
step_start = time.time()
query_embedding = self._generate_embedding(query)
print(f" [1] Generate query embedding: {time.time() - step_start:.3f}s")
try:
# Step 1: Generate query embedding (CPU-bound, no DB needed)
step_start = time.time()
query_embedding = self._generate_embedding(query)
step_duration = time.time() - step_start
print(f" [1] Generate query embedding: {step_duration:.3f}s")
if tracer:
tracer.record_query_embedding(query_embedding)
tracer.add_phase_metric("generate_query_embedding", step_duration)
# Step 2: Find entry points (acquire connection only for this query)
step_start = time.time()
query_embedding_str = str(query_embedding)
# Log connection acquisition
conn_acquire_start = time.time()
async with pool.acquire() as conn:
conn_acquire_time = time.time() - conn_acquire_start
if conn_acquire_time > 0.1: # Log if waiting > 100ms
print(f" [2.1] Waited {conn_acquire_time:.3f}s for connection (pool busy)")
# Step 2: Find entry points
step_start = time.time()
# Convert embedding to string for asyncpg
query_embedding_str = str(query_embedding)
entry_points = await conn.fetch(
"""
SELECT id, text, context, event_date, access_count,
SELECT id, text, context, event_date, access_count, embedding,
1 - (embedding <=> $1::vector) AS similarity
FROM memory_units
WHERE agent_id = $2
@ -571,59 +667,82 @@ class TemporalSemanticMemory:
query_embedding_str, agent_id
)
print(f" [2] Find entry points: {len(entry_points)} found in {time.time() - step_start:.3f}s")
step_duration = time.time() - step_start
print(f" [2] Find entry points: {len(entry_points)} found in {step_duration:.3f}s")
if not entry_points:
print(f"[SEARCH] Complete: 0 results in {time.time() - search_start:.3f}s")
return []
# Step 3: Spreading activation with budget
step_start = time.time()
visited = set()
results = []
budget_remaining = thinking_budget
# Initialize entry points with their actual similarity scores instead of 1.0
queue = [(dict(unit), unit["similarity"], True) for unit in entry_points] # (unit, activation, is_entry)
# Track substep timings
update_access_time = 0
calculate_weight_time = 0
query_neighbors_time = 0
process_neighbors_time = 0
# Process nodes in batches for efficient neighbor querying
BATCH_SIZE = 50
nodes_to_process = [] # (unit, activation, is_entry_point)
while queue and budget_remaining > 0:
# Collect a batch of nodes to process
while queue and len(nodes_to_process) < BATCH_SIZE and budget_remaining > 0:
current_unit, activation, is_entry_point = queue.pop(0)
unit_id = str(current_unit["id"])
if unit_id not in visited:
visited.add(unit_id)
budget_remaining -= 1
nodes_to_process.append((current_unit, activation, is_entry_point))
if not nodes_to_process:
break
# Update access counts for batch
substep_start = time.time()
node_ids = [str(node[0]["id"]) for node in nodes_to_process]
await conn.execute(
"UPDATE memory_units SET access_count = access_count + 1 WHERE id::text = ANY($1)",
node_ids
if tracer:
tracer.add_phase_metric("find_entry_points", step_duration, {"count": len(entry_points)})
for rank, ep in enumerate(entry_points, 1):
tracer.add_entry_point(
node_id=str(ep["id"]),
text=ep["text"],
similarity=ep["similarity"],
rank=rank
)
update_access_time += time.time() - substep_start
# Query neighbors for ALL nodes in batch at once
if not entry_points:
print(f"[SEARCH] Complete: 0 results in {time.time() - search_start:.3f}s")
if tracer:
trace = tracer.finalize([])
return [], trace
return [], None
# Step 3: Spreading activation with budget (in-memory processing)
step_start = time.time()
visited = set()
results = []
budget_remaining = thinking_budget
# Initialize entry points with their actual similarity scores instead of 1.0
# Format: (unit, activation, is_entry, parent_node_id, link_type, link_weight)
queue = [(dict(unit), unit["similarity"], True, None, None, None) for unit in entry_points]
# Track substep timings
calculate_weight_time = 0
query_neighbors_time = 0
process_neighbors_time = 0
# Track which nodes were visited for deferred access count update
visited_node_ids = []
# Process nodes in batches for efficient neighbor querying
BATCH_SIZE = 50
nodes_to_process = [] # (unit, activation, is_entry_point, parent_node_id, link_type, link_weight)
while queue and budget_remaining > 0:
# Collect a batch of nodes to process (in-memory, no DB)
while queue and len(nodes_to_process) < BATCH_SIZE and budget_remaining > 0:
current_unit, activation, is_entry_point, parent_node_id, link_type, link_weight = queue.pop(0)
unit_id = str(current_unit["id"])
if unit_id not in visited:
visited.add(unit_id)
budget_remaining -= 1
nodes_to_process.append((current_unit, activation, is_entry_point, parent_node_id, link_type, link_weight))
visited_node_ids.append(unit_id) # Track for deferred update
elif tracer:
# Node already visited - prune
tracer.prune_node(unit_id, "already_visited", activation)
if not nodes_to_process:
break
# Acquire connection ONLY for neighbor queries (defer access count updates)
node_ids = [str(node[0]["id"]) for node in nodes_to_process]
# Log connection acquisition for batch queries
batch_conn_start = time.time()
async with pool.acquire() as conn:
batch_conn_acquire = time.time() - batch_conn_start
if batch_conn_acquire > 0.1: # Log if waiting > 100ms
print(f" [3.3.1] Waited {batch_conn_acquire:.3f}s for connection (pool busy) - batch size: {len(node_ids)}")
# Query neighbors for ALL nodes in batch at once (without embeddings for speed)
substep_start = time.time()
all_neighbors = await conn.fetch(
"""
SELECT ml.from_unit_id, ml.to_unit_id, ml.weight,
mu.text, mu.context, mu.event_date, mu.access_count
SELECT ml.from_unit_id, ml.to_unit_id, ml.weight, ml.link_type, ml.entity_id,
mu.text, mu.context, mu.event_date, mu.access_count,
mu.id as neighbor_id
FROM memory_links ml
JOIN memory_units mu ON ml.to_unit_id = mu.id
WHERE ml.from_unit_id::text = ANY($1)
@ -632,116 +751,190 @@ class TemporalSemanticMemory:
""",
node_ids
)
query_neighbors_time += time.time() - substep_start
neighbor_query_time = time.time() - substep_start
if neighbor_query_time > 1.0: # Log slow neighbor queries
print(f" [3.3.3] Slow NEIGHBOR query: {neighbor_query_time:.3f}s for {len(node_ids)} nodes → {len(all_neighbors)} neighbors")
query_neighbors_time += neighbor_query_time
# Group neighbors by from_unit_id
# Fetch embeddings for current batch nodes (needed for weight calculation)
substep_start = time.time()
neighbors_by_node = {}
for neighbor in all_neighbors:
from_id = str(neighbor["from_unit_id"])
if from_id not in neighbors_by_node:
neighbors_by_node[from_id] = []
neighbors_by_node[from_id].append(neighbor)
embeddings = await conn.fetch(
"SELECT id, embedding FROM memory_units WHERE id::text = ANY($1)",
node_ids
)
embedding_map = {str(row["id"]): row["embedding"] for row in embeddings}
fetch_embeddings_time = time.time() - substep_start
if fetch_embeddings_time > 0.5:
print(f" [3.3.4] Slow EMBEDDING fetch: {fetch_embeddings_time:.3f}s for {len(node_ids)} nodes")
query_neighbors_time += fetch_embeddings_time
# Process each node in the batch
for current_unit, activation, is_entry_point in nodes_to_process:
unit_id = str(current_unit["id"])
# Group neighbors by from_unit_id (in-memory, no DB)
substep_start = time.time()
neighbors_by_node = {}
for neighbor in all_neighbors:
from_id = str(neighbor["from_unit_id"])
if from_id not in neighbors_by_node:
neighbors_by_node[from_id] = []
neighbors_by_node[from_id].append(neighbor)
# Calculate combined weight
event_date = current_unit["event_date"]
days_since = (utcnow() - event_date).total_seconds() / 86400
# Process each node in the batch (CPU-bound, no DB)
for current_unit, activation, is_entry_point, parent_node_id, parent_link_type, parent_link_weight in nodes_to_process:
unit_id = str(current_unit["id"])
recency_weight = calculate_recency_weight(days_since)
frequency_weight = calculate_frequency_weight(current_unit.get("access_count", 0))
# Calculate combined weight
event_date = current_unit["event_date"]
days_since = (utcnow() - event_date).total_seconds() / 86400
# Normalize frequency to [0, 1] range
frequency_normalized = (frequency_weight - 1.0) / 1.0
recency_weight = calculate_recency_weight(days_since)
frequency_weight = calculate_frequency_weight(current_unit.get("access_count", 0))
# Calculate semantic similarity between query and this memory
memory_embedding = current_unit.get("embedding")
if memory_embedding is not None:
# Cosine similarity = 1 - cosine distance
query_vec = np.array(query_embedding)
memory_vec = np.array(memory_embedding)
# Cosine similarity
dot_product = np.dot(query_vec, memory_vec)
norm_query = np.linalg.norm(query_vec)
norm_memory = np.linalg.norm(memory_vec)
semantic_similarity = dot_product / (norm_query * norm_memory) if norm_query > 0 and norm_memory > 0 else 0.0
else:
semantic_similarity = 0.0
# Normalize frequency to [0, 1] range
frequency_normalized = (frequency_weight - 1.0) / 1.0
# Combined weight: 30% activation, 30% semantic similarity, 25% recency, 15% frequency
final_weight = 0.3 * activation + 0.3 * semantic_similarity + 0.25 * recency_weight + 0.15 * frequency_normalized
# Calculate semantic similarity between query and this memory
# Get embedding from the map we fetched
memory_embedding = embedding_map.get(unit_id)
if memory_embedding is not None:
# Convert embedding to list of floats if it's a string or other type
if isinstance(memory_embedding, str):
import json
memory_embedding = json.loads(memory_embedding)
elif not isinstance(memory_embedding, (list, np.ndarray)):
# If it's some other type, try to convert it
memory_embedding = list(memory_embedding)
# Notify tracer
if live_tracer:
live_tracer.visit_node(
node_id=unit_id,
text=current_unit["text"],
activation=activation,
recency=recency_weight,
frequency=frequency_weight,
weight=final_weight,
is_entry_point=is_entry_point,
)
# Cosine similarity = 1 - cosine distance
query_vec = np.array(query_embedding, dtype=np.float64)
memory_vec = np.array(memory_embedding, dtype=np.float64)
# Cosine similarity
dot_product = np.dot(query_vec, memory_vec)
norm_query = np.linalg.norm(query_vec)
norm_memory = np.linalg.norm(memory_vec)
semantic_similarity = dot_product / (norm_query * norm_memory) if norm_query > 0 and norm_memory > 0 else 0.0
else:
semantic_similarity = 0.0
results.append({
"id": unit_id,
"text": current_unit["text"],
"context": current_unit.get("context", ""),
"event_date": event_date.isoformat(),
"weight": final_weight,
"activation": activation,
"semantic_similarity": semantic_similarity,
"recency": recency_weight,
"frequency": frequency_weight,
})
# Combined weight using configurable parameters
final_weight = (
weight_activation * activation +
weight_semantic * semantic_similarity +
weight_recency * recency_weight +
weight_frequency * frequency_normalized
)
# Spread to neighbors (from batch query results)
neighbors = neighbors_by_node.get(unit_id, [])
for neighbor in neighbors:
neighbor_id = str(neighbor["to_unit_id"])
if neighbor_id not in visited:
link_weight = neighbor["weight"]
new_activation = activation * link_weight * 0.8 # 0.8 = decay factor
# Notify tracer
if tracer:
tracer.visit_node(
node_id=unit_id,
text=current_unit["text"],
context=current_unit.get("context", ""),
event_date=event_date,
access_count=current_unit.get("access_count", 0),
is_entry_point=is_entry_point,
parent_node_id=parent_node_id,
link_type=parent_link_type,
link_weight=parent_link_weight,
activation=activation,
semantic_similarity=semantic_similarity,
recency=recency_weight,
frequency=frequency_normalized,
final_weight=final_weight,
)
if new_activation > 0.1:
queue.append(({
"id": neighbor["to_unit_id"],
"text": neighbor["text"],
"context": neighbor.get("context", ""),
"event_date": neighbor["event_date"],
"access_count": neighbor["access_count"],
"embedding": neighbor.get("embedding"),
}, new_activation, False)) # Not an entry point
results.append({
"id": unit_id,
"text": current_unit["text"],
"context": current_unit.get("context", ""),
"event_date": event_date.isoformat(),
"weight": final_weight,
"activation": activation,
"semantic_similarity": semantic_similarity,
"recency": recency_weight,
"frequency": frequency_weight,
})
calculate_weight_time += time.time() - substep_start
process_neighbors_time += time.time() - substep_start
# Spread to neighbors (from batch query results)
neighbors = neighbors_by_node.get(unit_id, [])
for neighbor in neighbors:
neighbor_id = str(neighbor["to_unit_id"])
link_weight = neighbor["weight"]
link_type = neighbor["link_type"]
entity_id = str(neighbor["entity_id"]) if neighbor["entity_id"] else None
new_activation = activation * link_weight * 0.8 # 0.8 = decay factor
# Clear batch for next iteration
nodes_to_process = []
if neighbor_id not in visited:
if new_activation > 0.1:
queue.append(({
"id": neighbor["to_unit_id"],
"text": neighbor["text"],
"context": neighbor.get("context", ""),
"event_date": neighbor["event_date"],
"access_count": neighbor["access_count"],
}, new_activation, False, unit_id, link_type, link_weight)) # parent_id, link_type, link_weight
spreading_activation_time = time.time() - step_start
num_batches = (len(visited) + BATCH_SIZE - 1) // BATCH_SIZE # Ceiling division
print(f" [3] Spreading activation: {len(visited)} nodes visited in {spreading_activation_time:.3f}s")
print(f" [3.1] Update access counts: {update_access_time:.3f}s")
print(f" [3.2] Calculate weights: {calculate_weight_time:.3f}s")
print(f" [3.3] Query neighbors: {query_neighbors_time:.3f}s ({num_batches} batched queries)")
print(f" [3.4] Process neighbors: {process_neighbors_time:.3f}s")
if tracer:
tracer.add_neighbor_link(
from_node_id=unit_id,
to_node_id=neighbor_id,
link_type=link_type,
link_weight=link_weight,
entity_id=entity_id,
new_activation=new_activation,
followed=True
)
elif tracer:
tracer.add_neighbor_link(
from_node_id=unit_id,
to_node_id=neighbor_id,
link_type=link_type,
link_weight=link_weight,
entity_id=entity_id,
new_activation=new_activation,
followed=False,
prune_reason="activation_too_low"
)
# Step 4: Sort by final weight and return top results
step_start = time.time()
results.sort(key=lambda x: x["weight"], reverse=True)
top_results = results[:top_k]
print(f" [4] Sort and return top {top_k}: {time.time() - step_start:.3f}s")
calculate_weight_time += time.time() - substep_start
process_neighbors_time += time.time() - substep_start
print(f"[SEARCH] Complete: {len(top_results)} results in {time.time() - search_start:.3f}s\n")
return top_results
# Clear batch for next iteration
nodes_to_process = []
except Exception as e:
print(f"[SEARCH] ERROR after {time.time() - search_start:.3f}s: {str(e)}")
raise Exception(f"Failed to search memories: {str(e)}")
spreading_activation_time = time.time() - step_start
num_batches = (len(visited) + BATCH_SIZE - 1) // BATCH_SIZE # Ceiling division
print(f" [3] Spreading activation: {len(visited)} nodes visited in {spreading_activation_time:.3f}s")
print(f" [3.1] Calculate weights: {calculate_weight_time:.3f}s")
print(f" [3.2] Query neighbors: {query_neighbors_time:.3f}s ({num_batches} batched queries)")
print(f" [3.3] Process neighbors: {process_neighbors_time:.3f}s")
if tracer:
tracer.add_phase_metric("spreading_activation", spreading_activation_time, {
"nodes_visited": len(visited),
"num_batches": num_batches
})
# Step 4: Queue access count updates (background worker will process them)
if visited_node_ids:
await self._access_count_queue.put(visited_node_ids)
print(f" [4] Queued access count updates for {len(visited_node_ids)} nodes")
# Step 5: Sort by final weight and return top results
step_start = time.time()
results.sort(key=lambda x: x["weight"], reverse=True)
top_results = results[:top_k]
print(f" [5] Sort and return top {top_k}: {time.time() - step_start:.3f}s")
print(f"[SEARCH] Complete: {len(top_results)} results in {time.time() - search_start:.3f}s\n")
# Finalize trace if enabled
if tracer:
trace = tracer.finalize(top_results)
return top_results, trace
return top_results, None
except Exception as e:
print(f"[SEARCH] ERROR after {time.time() - search_start:.3f}s: {str(e)}")
raise Exception(f"Failed to search memories: {str(e)}")
async def delete_agent(self, agent_id: str) -> Dict[str, int]:
"""

View file

@ -62,22 +62,36 @@ def cosine_similarity(vec1: List[float], vec2: List[float]) -> float:
return dot_product / (magnitude1 * magnitude2)
def calculate_recency_weight(days_since: float, decay_rate: float = 0.1) -> float:
def calculate_recency_weight(days_since: float, half_life_days: float = 365.0) -> float:
"""
Calculate recency weight with exponential decay.
Calculate recency weight using logarithmic decay.
Recent memories are weighted higher. The decay rate controls
how quickly old memories fade.
This provides much better differentiation over long time periods compared to
exponential decay. Uses a log-based decay where the half-life parameter controls
when memories reach 50% weight.
Examples:
- Today (0 days): 1.0
- 1 year (365 days): ~0.5 (with default half_life=365)
- 2 years (730 days): ~0.33
- 5 years (1825 days): ~0.17
- 10 years (3650 days): ~0.09
This ensures that 2-year-old and 5-year-old memories have meaningfully
different weights, unlike exponential decay which makes them both ~0.
Args:
days_since: Number of days since the memory was created
decay_rate: How quickly memories fade (higher = faster decay)
half_life_days: Number of days for weight to reach 0.5 (default: 1 year)
Returns:
Weight between 0 and 1
"""
import math
return math.exp(-decay_rate * days_since)
# Logarithmic decay: 1 / (1 + log(1 + days_since/half_life))
# This decays much slower than exponential, giving better long-term differentiation
normalized_age = days_since / half_life_days
return 1.0 / (1.0 + math.log1p(normalized_age))
def calculate_frequency_weight(access_count: int, max_boost: float = 2.0) -> float:

View file

@ -163,232 +163,3 @@ class MemoryVisualizer:
self.console.print(f"[green]✓[/green] Memory graph saved to [cyan]{output_file}[/cyan]")
class LiveSearchTracer:
"""
Live tracer for search operations showing spreading activation in real-time.
"""
def __init__(self):
"""Initialize the live tracer."""
self.console = Console()
self.visited_nodes = []
self.current_node = None
self.search_results = []
self.query = ""
self.budget_used = 0
self.budget_total = 0
def start_search(self, query: str, budget: int):
"""
Start a new search trace.
Args:
query: Search query
budget: Thinking budget
"""
self.query = query
self.budget_total = budget
self.budget_used = 0
self.visited_nodes = []
self.current_node = None
self.search_results = []
def visit_node(
self,
node_id: str,
text: str,
activation: float,
recency: float,
frequency: float,
weight: float,
is_entry_point: bool = False,
):
"""
Record a node visit.
Args:
node_id: Node ID
text: Node text
activation: Activation strength
recency: Recency weight
frequency: Frequency weight
weight: Combined weight
is_entry_point: Whether this is an entry point
"""
self.current_node = {
'id': node_id,
'text': text,
'activation': activation,
'recency': recency,
'frequency': frequency,
'weight': weight,
'is_entry_point': is_entry_point,
}
self.visited_nodes.append(self.current_node)
self.budget_used += 1
def add_result(
self,
text: str,
weight: float,
activation: float,
recency: float,
frequency: float,
):
"""
Add a search result.
Args:
text: Result text
weight: Combined weight
activation: Activation strength
recency: Recency weight
frequency: Frequency weight
"""
self.search_results.append({
'text': text,
'weight': weight,
'activation': activation,
'recency': recency,
'frequency': frequency,
})
def render_live(self) -> Layout:
"""
Render the current state.
Returns:
Rich Layout with current state
"""
layout = Layout()
layout.split_column(
Layout(name="header", size=3),
Layout(name="body"),
Layout(name="footer", size=5)
)
# Header
header_text = Text()
header_text.append("🔍 ", style="bold cyan")
header_text.append(f"Query: ", style="bold white")
header_text.append(f"{self.query}", style="bold yellow")
layout["header"].update(Panel(header_text, style="cyan"))
# Body - split into current node and visited
layout["body"].split_row(
Layout(name="current", ratio=1),
Layout(name="path", ratio=1),
)
# Current node
if self.current_node:
current_table = Table(
title="Current Node",
show_header=False,
box=box.ROUNDED,
style="green"
)
current_table.add_column("Key", style="cyan")
current_table.add_column("Value", style="white")
status = "🎯 ENTRY POINT" if self.current_node['is_entry_point'] else "🔄 EXPLORING"
current_table.add_row("Status", status)
current_table.add_row("Text", self.current_node['text'][:50] + "...")
current_table.add_row(
"Weights",
f"A:{self.current_node['activation']:.2f} "
f"R:{self.current_node['recency']:.2f} "
f"F:{self.current_node['frequency']:.2f}"
)
current_table.add_row(
"Combined",
f"[bold yellow]{self.current_node['weight']:.3f}[/bold yellow]"
)
layout["current"].update(Panel(current_table, border_style="green"))
else:
layout["current"].update(Panel("Initializing...", border_style="dim"))
# Visited path
path_table = Table(
title=f"Visited Nodes ({len(self.visited_nodes)})",
box=box.SIMPLE,
show_header=True,
style="blue"
)
path_table.add_column("#", style="dim", width=4)
path_table.add_column("Text", style="white", width=35)
path_table.add_column("Weight", justify="right", style="yellow", width=8)
path_table.add_column("Type", style="cyan", width=8)
for i, node in enumerate(reversed(self.visited_nodes[-10:])): # Last 10
node_type = "ENTRY" if node['is_entry_point'] else "SPREAD"
path_table.add_row(
str(len(self.visited_nodes) - i),
node['text'][:32] + "...",
f"{node['weight']:.3f}",
node_type
)
layout["path"].update(Panel(path_table, border_style="blue"))
# Footer - progress bar
progress = self.budget_used / self.budget_total if self.budget_total > 0 else 0
bar_width = 50
filled = int(bar_width * progress)
bar = "" * filled + "" * (bar_width - filled)
footer_text = Text()
footer_text.append(f"Progress: ", style="bold white")
footer_text.append(bar, style="yellow")
footer_text.append(f" {self.budget_used}/{self.budget_total}", style="bold cyan")
footer_text.append(f" ({progress*100:.1f}%)", style="dim")
layout["footer"].update(Panel(footer_text, style="yellow"))
return layout
def show_final_results(self):
"""
Show final search results in a nice table.
"""
self.console.print("\n")
results_table = Table(
title="🎯 Search Results",
show_header=True,
header_style="bold magenta",
box=box.DOUBLE_EDGE,
title_style="bold white"
)
results_table.add_column("Rank", style="cyan", justify="center", width=6)
results_table.add_column("Text", style="white", width=50)
results_table.add_column("Weight", justify="right", style="yellow", width=8)
results_table.add_column("A", justify="right", style="green", width=6)
results_table.add_column("R", justify="right", style="blue", width=6)
results_table.add_column("F", justify="right", style="magenta", width=6)
for i, result in enumerate(self.search_results, 1):
rank_style = "bold yellow" if i <= 3 else "cyan"
results_table.add_row(
f"#{i}",
result['text'][:47] + "...",
f"{result['weight']:.3f}",
f"{result['activation']:.2f}",
f"{result['recency']:.2f}",
f"{result['frequency']:.2f}",
style=rank_style if i <= 3 else None
)
self.console.print(results_table)
# Summary stats
summary = Table.grid(padding=(0, 2))
summary.add_column(style="bold cyan")
summary.add_column(style="white")
summary.add_row("Total nodes visited:", f"{len(self.visited_nodes)}")
summary.add_row("Budget used:", f"{self.budget_used}/{self.budget_total}")
summary.add_row("Results found:", f"{len(self.search_results)}")
self.console.print(Panel(summary, title="Summary", border_style="green", padding=(1, 2)))

View file

@ -0,0 +1,8 @@
-- Migration: Add composite index for spreading activation neighbor queries
-- This index optimizes the WHERE ml.from_unit_id::text = ANY($1) AND ml.weight >= 0.1 query
-- which is used during spreading activation search.
-- Composite index for spreading activation neighbor queries (from_unit_id + weight filter)
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_memory_links_from_weight
ON memory_links(from_unit_id, weight DESC)
WHERE weight >= 0.1;

View file

@ -19,4 +19,7 @@ dependencies = [
"pytest>=7.0.0",
"pytest-asyncio>=0.21.0",
"langchain-text-splitters>=0.3.0",
"flask>=3.1.2",
"fastapi[standard]>=0.120.3",
"uvicorn>=0.38.0",
]

View file

@ -97,3 +97,7 @@ CREATE INDEX IF NOT EXISTS idx_memory_links_from ON memory_links(from_unit_id);
CREATE INDEX IF NOT EXISTS idx_memory_links_to ON memory_links(to_unit_id);
CREATE INDEX IF NOT EXISTS idx_memory_links_type ON memory_links(link_type);
CREATE INDEX IF NOT EXISTS idx_memory_links_entity ON memory_links(entity_id) WHERE entity_id IS NOT NULL;
-- Composite index for spreading activation neighbor queries (from_unit_id + weight filter)
CREATE INDEX IF NOT EXISTS idx_memory_links_from_weight ON memory_links(from_unit_id, weight DESC)
WHERE weight >= 0.1;

3
serve.sh Executable file
View file

@ -0,0 +1,3 @@
#!/bin/bash
# Start the FastAPI server with hot reload
uv run uvicorn web.server:app --reload --host 0.0.0.0 --port 8080

185
tests/test_search_trace.py Normal file
View file

@ -0,0 +1,185 @@
"""
Test search tracing functionality.
"""
import pytest
import asyncio
import os
from memory.temporal_semantic_memory import TemporalSemanticMemory
from memory.search_trace import SearchTrace
from datetime import datetime, timezone
@pytest.mark.asyncio
async def test_search_with_trace():
"""Test that search with enable_trace=True returns a valid SearchTrace."""
# Use test database
db_url = os.getenv("DATABASE_URL")
if not db_url:
pytest.skip("DATABASE_URL not set")
memory = TemporalSemanticMemory(db_url=db_url)
try:
# Generate a unique agent ID for this test
agent_id = f"test_trace_{datetime.now(timezone.utc).timestamp()}"
# Store some test memories
await memory.put_async(
agent_id=agent_id,
content="Alice works at Google in Mountain View",
context="test context",
)
await memory.put_async(
agent_id=agent_id,
content="Bob also works at Google but in New York",
context="test context",
)
await memory.put_async(
agent_id=agent_id,
content="Charlie founded a startup called TechCorp",
context="test context",
)
# Search with tracing enabled
results, trace = await memory.search_async(
agent_id=agent_id,
query="Who works at Google?",
thinking_budget=20,
top_k=5,
enable_trace=True,
)
# Verify results
assert len(results) > 0, "Should have search results"
# Verify trace object
assert trace is not None, "Trace should not be None when enable_trace=True"
assert isinstance(trace, SearchTrace), "Trace should be SearchTrace instance"
# Verify query info
assert trace.query.query_text == "Who works at Google?"
assert trace.query.thinking_budget == 20
assert trace.query.top_k == 5
assert len(trace.query.query_embedding) > 0, "Query embedding should be populated"
# Verify entry points
assert len(trace.entry_points) > 0, "Should have entry points"
for ep in trace.entry_points:
assert ep.node_id, "Entry point should have node_id"
assert ep.text, "Entry point should have text"
assert 0.0 <= ep.similarity_score <= 1.0, "Similarity should be in [0, 1]"
# Verify visits
assert len(trace.visits) > 0, "Should have visited nodes"
for visit in trace.visits:
assert visit.node_id, "Visit should have node_id"
assert visit.text, "Visit should have text"
assert visit.weights.final_weight >= 0, "Weight should be non-negative"
# Entry points should have no parent
if visit.is_entry_point:
assert visit.parent_node_id is None
assert visit.link_type is None
else:
# Non-entry points should have parent info (unless they're isolated)
# But we allow None parent if the node was reached differently
pass
# Verify summary
assert trace.summary.total_nodes_visited == len(trace.visits)
assert trace.summary.results_returned == len(results)
assert trace.summary.budget_used <= trace.query.thinking_budget
assert trace.summary.total_duration_seconds > 0
# Verify phase metrics
assert len(trace.summary.phase_metrics) > 0, "Should have phase metrics"
phase_names = {pm.phase_name for pm in trace.summary.phase_metrics}
assert "generate_query_embedding" in phase_names
assert "find_entry_points" in phase_names
assert "spreading_activation" in phase_names
# Test JSON export
json_str = trace.to_json()
assert json_str, "Should be able to export to JSON"
assert "query" in json_str
assert "visits" in json_str
assert "summary" in json_str
# Test dict export
trace_dict = trace.to_dict()
assert isinstance(trace_dict, dict)
assert "query" in trace_dict
assert "visits" in trace_dict
# Test helper methods
if len(trace.visits) > 0:
first_visit = trace.visits[0]
found_visit = trace.get_visit_by_node_id(first_visit.node_id)
assert found_visit is not None
assert found_visit.node_id == first_visit.node_id
# Test get_entry_point_nodes
entry_point_visits = trace.get_entry_point_nodes()
assert len(entry_point_visits) > 0
for epv in entry_point_visits:
assert epv.is_entry_point
print("\n✓ Search trace test passed!")
print(f" - Query: {trace.query.query_text}")
print(f" - Entry points: {len(trace.entry_points)}")
print(f" - Nodes visited: {trace.summary.total_nodes_visited}")
print(f" - Nodes pruned: {trace.summary.total_nodes_pruned}")
print(f" - Results returned: {trace.summary.results_returned}")
print(f" - Duration: {trace.summary.total_duration_seconds:.3f}s")
# Cleanup
await memory.delete_agent(agent_id)
finally:
await memory.close()
@pytest.mark.asyncio
async def test_search_without_trace():
"""Test that search with enable_trace=False returns None for trace."""
db_url = os.getenv("DATABASE_URL")
if not db_url:
pytest.skip("DATABASE_URL not set")
memory = TemporalSemanticMemory(db_url=db_url)
try:
agent_id = f"test_no_trace_{datetime.now(timezone.utc).timestamp()}"
# Store a test memory
await memory.put_async(
agent_id=agent_id,
content="Test memory without trace",
context="test",
)
# Search without tracing
results, trace = await memory.search_async(
agent_id=agent_id,
query="test",
thinking_budget=10,
top_k=5,
enable_trace=False,
)
# Verify trace is None
assert trace is None, "Trace should be None when enable_trace=False"
assert isinstance(results, list), "Results should still be a list"
print("\n✓ Search without trace test passed!")
# Cleanup
await memory.delete_agent(agent_id)
finally:
await memory.close()
if __name__ == "__main__":
# Run tests directly
asyncio.run(test_search_with_trace())
asyncio.run(test_search_without_trace())

505
uv.lock
View file

@ -6,6 +6,15 @@ resolution-markers = [
"python_full_version < '3.12'",
]
[[package]]
name = "annotated-doc"
version = "0.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/a6/dc46877b911e40c00d395771ea710d5e77b6de7bacd5fdcd78d70cc5a48f/annotated_doc-0.0.3.tar.gz", hash = "sha256:e18370014c70187422c33e945053ff4c286f453a984eba84d0dbfa0c935adeda", size = 5535 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/02/b7/cf592cb5de5cb3bade3357f8d2cf42bf103bbe39f459824b4939fd212911/annotated_doc-0.0.3-py3-none-any.whl", hash = "sha256:348ec6664a76f1fd3be81f43dffbee4c7e8ce931ba71ec67cc7f4ade7fbbb580", size = 5488 },
]
[[package]]
name = "annotated-types"
version = "0.7.0"
@ -61,6 +70,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/a4/cec76b3389c4c5ff66301cd100fe88c318563ec8a520e0b2e792b5b84972/asyncpg-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:f59b430b8e27557c3fb9869222559f7417ced18688375825f8f12302c34e915e", size = 621623 },
]
[[package]]
name = "blinker"
version = "1.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458 },
]
[[package]]
name = "blis"
version = "1.3.0"
@ -362,6 +380,91 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277 },
]
[[package]]
name = "dnspython"
version = "2.8.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094 },
]
[[package]]
name = "email-validator"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "dnspython" },
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604 },
]
[[package]]
name = "fastapi"
version = "0.120.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-doc" },
{ name = "pydantic" },
{ name = "starlette" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/85/c6/f324c07f5ebe34237b56b6396a94568d2d4a705df8a2ff82fa45029e7252/fastapi-0.120.3.tar.gz", hash = "sha256:17db50718ee86c9e01e54f9d8600abf130f6f762711cd0d8f02eb392668271ba", size = 339363 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/37/3a/1eef3ab55ede5af09186723898545a94d0a32b7ac9ea4e7af7bcb95f132a/fastapi-0.120.3-py3-none-any.whl", hash = "sha256:bfee21c98db9128dc425a686eafd14899e26e4471aab33076bff2427fd6dcd22", size = 108255 },
]
[package.optional-dependencies]
standard = [
{ name = "email-validator" },
{ name = "fastapi-cli", extra = ["standard"] },
{ name = "httpx" },
{ name = "jinja2" },
{ name = "python-multipart" },
{ name = "uvicorn", extra = ["standard"] },
]
[[package]]
name = "fastapi-cli"
version = "0.0.14"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "rich-toolkit" },
{ name = "typer" },
{ name = "uvicorn", extra = ["standard"] },
]
sdist = { url = "https://files.pythonhosted.org/packages/cc/13/11e43d630be84e51ba5510a6da6a11eb93b44b72caa796137c5dddda937b/fastapi_cli-0.0.14.tar.gz", hash = "sha256:ddfb5de0a67f77a8b3271af1460489bd4d7f4add73d11fbfac613827b0275274", size = 17994 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/40/e8/bc8bbfd93dcc8e347ce98a3e654fb0d2e5f2739afb46b98f41a30c339269/fastapi_cli-0.0.14-py3-none-any.whl", hash = "sha256:e66b9ad499ee77a4e6007545cde6de1459b7f21df199d7f29aad2adaab168eca", size = 11151 },
]
[package.optional-dependencies]
standard = [
{ name = "fastapi-cloud-cli" },
{ name = "uvicorn", extra = ["standard"] },
]
[[package]]
name = "fastapi-cloud-cli"
version = "0.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
{ name = "pydantic", extra = ["email"] },
{ name = "rich-toolkit" },
{ name = "rignore" },
{ name = "sentry-sdk" },
{ name = "typer" },
{ name = "uvicorn", extra = ["standard"] },
]
sdist = { url = "https://files.pythonhosted.org/packages/f9/48/0f14d8555b750dc8c04382804e4214f1d7f55298127f3a0237ba566e69dd/fastapi_cloud_cli-0.3.1.tar.gz", hash = "sha256:8c7226c36e92e92d0c89827e8f56dbf164ab2de4444bd33aa26b6c3f7675db69", size = 24080 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/68/79/7f5a5e5513e6a737e5fb089d9c59c74d4d24dc24d581d3aa519b326bedda/fastapi_cloud_cli-0.3.1-py3-none-any.whl", hash = "sha256:7d1a98a77791a9d0757886b2ffbf11bcc6b3be93210dd15064be10b216bf7e00", size = 19711 },
]
[[package]]
name = "filelock"
version = "3.20.0"
@ -371,6 +474,23 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054 },
]
[[package]]
name = "flask"
version = "3.1.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "blinker" },
{ name = "click" },
{ name = "itsdangerous" },
{ name = "jinja2" },
{ name = "markupsafe" },
{ name = "werkzeug" },
]
sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308 },
]
[[package]]
name = "fonttools"
version = "4.60.1"
@ -480,6 +600,42 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 },
]
[[package]]
name = "httptools"
version = "0.7.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9c/08/17e07e8d89ab8f343c134616d72eebfe03798835058e2ab579dcc8353c06/httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657", size = 206521 },
{ url = "https://files.pythonhosted.org/packages/aa/06/c9c1b41ff52f16aee526fd10fbda99fa4787938aa776858ddc4a1ea825ec/httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70", size = 110375 },
{ url = "https://files.pythonhosted.org/packages/cc/cc/10935db22fda0ee34c76f047590ca0a8bd9de531406a3ccb10a90e12ea21/httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df", size = 456621 },
{ url = "https://files.pythonhosted.org/packages/0e/84/875382b10d271b0c11aa5d414b44f92f8dd53e9b658aec338a79164fa548/httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e", size = 454954 },
{ url = "https://files.pythonhosted.org/packages/30/e1/44f89b280f7e46c0b1b2ccee5737d46b3bb13136383958f20b580a821ca0/httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274", size = 440175 },
{ url = "https://files.pythonhosted.org/packages/6f/7e/b9287763159e700e335028bc1824359dc736fa9b829dacedace91a39b37e/httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec", size = 440310 },
{ url = "https://files.pythonhosted.org/packages/b3/07/5b614f592868e07f5c94b1f301b5e14a21df4e8076215a3bccb830a687d8/httptools-0.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb", size = 86875 },
{ url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280 },
{ url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004 },
{ url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655 },
{ url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440 },
{ url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186 },
{ url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192 },
{ url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694 },
{ url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889 },
{ url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180 },
{ url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596 },
{ url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268 },
{ url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517 },
{ url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337 },
{ url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743 },
{ url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619 },
{ url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714 },
{ url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909 },
{ url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831 },
{ url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631 },
{ url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910 },
{ url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205 },
]
[[package]]
name = "httpx"
version = "0.28.1"
@ -532,6 +688,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 },
]
[[package]]
name = "itsdangerous"
version = "2.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234 },
]
[[package]]
name = "jinja2"
version = "3.1.6"
@ -1042,6 +1207,8 @@ version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "asyncpg" },
{ name = "fastapi", extra = ["standard"] },
{ name = "flask" },
{ name = "langchain-text-splitters" },
{ name = "matplotlib" },
{ name = "networkx" },
@ -1055,11 +1222,14 @@ dependencies = [
{ name = "sentence-transformers" },
{ name = "spacy" },
{ name = "torch" },
{ name = "uvicorn" },
]
[package.metadata]
requires-dist = [
{ name = "asyncpg", specifier = ">=0.29.0" },
{ name = "fastapi", extras = ["standard"], specifier = ">=0.120.3" },
{ name = "flask", specifier = ">=3.1.2" },
{ name = "langchain-text-splitters", specifier = ">=0.3.0" },
{ name = "matplotlib", specifier = ">=3.7.0" },
{ name = "networkx", specifier = ">=3.0" },
@ -1073,6 +1243,7 @@ requires-dist = [
{ name = "sentence-transformers", specifier = ">=2.2.0" },
{ name = "spacy", specifier = ">=3.7.0" },
{ name = "torch", specifier = ">=2.0.0" },
{ name = "uvicorn", specifier = ">=0.38.0" },
]
[[package]]
@ -1592,6 +1763,11 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a1/6b/83661fa77dcefa195ad5f8cd9af3d1a7450fd57cc883ad04d65446ac2029/pydantic-2.12.3-py3-none-any.whl", hash = "sha256:6986454a854bc3bc6e5443e1369e06a3a456af9d339eda45510f517d9ea5c6bf", size = 462431 },
]
[package.optional-dependencies]
email = [
{ name = "email-validator" },
]
[[package]]
name = "pydantic-core"
version = "2.41.4"
@ -1753,6 +1929,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230 },
]
[[package]]
name = "python-multipart"
version = "0.0.20"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546 },
]
[[package]]
name = "pyyaml"
version = "6.0.3"
@ -1940,6 +2125,97 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393 },
]
[[package]]
name = "rich-toolkit"
version = "0.15.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "rich" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/67/33/1a18839aaa8feef7983590c05c22c9c09d245ada6017d118325bbfcc7651/rich_toolkit-0.15.1.tar.gz", hash = "sha256:6f9630eb29f3843d19d48c3bd5706a086d36d62016687f9d0efa027ddc2dd08a", size = 115322 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/49/42821d55ead7b5a87c8d121edf323cb393d8579f63e933002ade900b784f/rich_toolkit-0.15.1-py3-none-any.whl", hash = "sha256:36a0b1d9a135d26776e4b78f1d5c2655da6e0ef432380b5c6b523c8d8ab97478", size = 29412 },
]
[[package]]
name = "rignore"
version = "0.7.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b1/b5/1fe06acc517315fba13db19039e77a2b9689451e0b5b02e03f26f05f24ec/rignore-0.7.2.tar.gz", hash = "sha256:b343749a59b53db30be1180ffab6995a914a244860e31a5cbea25bb647c38a61", size = 15254 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/1e/34f11b4ebc331fc8f137d2b65304667a58bd2b321ce6309ac1e6f7f1c9b2/rignore-0.7.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ed6ec2d50664865feea344b2e39eaad697f0f2b1676a26add1b458e416120a2b", size = 891771 },
{ url = "https://files.pythonhosted.org/packages/d0/a8/4240d08eb693908451bcb6efc27e1ae936dee8b1adfd5fbcc7f7668fb961/rignore-0.7.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9bad1790c32bf1f84bed6f2750933cfe67be056da074ed98a8808f7fb6d0aae0", size = 823881 },
{ url = "https://files.pythonhosted.org/packages/19/a7/162a821b67e3ef0444c8713ae28c6a66f3ffed29197b3927ae5513020591/rignore-0.7.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ac56757af3b224ffb20368f033007404676d4e211d85a0b95b6b57cd94898ec", size = 901542 },
{ url = "https://files.pythonhosted.org/packages/3c/b3/6d5ae8b7b2fb94f5a3962e281843c983c8ed0a57bc37ac8ef893f581e460/rignore-0.7.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:11284ae105e5e80539a420b194d8624940f7836caafb9cde45e2b590ed957ebf", size = 874673 },
{ url = "https://files.pythonhosted.org/packages/4c/89/83063fb4d4b57d00cb9b6a04878e5971830518c001bb44ad1b93ace4f476/rignore-0.7.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5643cf2857a80744bd43752fac245de5f65effaafb0bac2736d62ded39b82d8", size = 1177702 },
{ url = "https://files.pythonhosted.org/packages/e9/53/6ac52ead4dbc99acb5a1de1794ef522f93dacf535d017469d64ff84f4262/rignore-0.7.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:193e6414107634546416fa80e0ca67c5e5899eb5fcd699f444138b8f25d557d1", size = 944091 },
{ url = "https://files.pythonhosted.org/packages/ec/7b/fcf87d8050f103e377357bd599d0d976d67623c1a1f87d454e9a97a7e605/rignore-0.7.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:059932ba1bf00130bce0ee8569e44b466fd4e249e3befffdca50843e0e63d7b5", size = 959575 },
{ url = "https://files.pythonhosted.org/packages/73/fa/d3aa50f33376c6ec44ba96af6d576aa5d5fb16087e39fb2646eca789ef05/rignore-0.7.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0de4bcf93471999260885c123ebac59ddab96257649b3a0cb7daefbfa7ed7714", size = 985688 },
{ url = "https://files.pythonhosted.org/packages/b2/58/761abdf261b4aefa0e41a5d52093fbc7e6a53ce52fdb6dc5403c9ad43558/rignore-0.7.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9049a42a9c34ee4f191038f653e14eb8bdaf3e913c7ebfc7cb0945896ab8cd2a", size = 1082297 },
{ url = "https://files.pythonhosted.org/packages/d2/01/d9a396d7535f3b0fce0f9e0f6317e498a70d801831dabf2c3648835e9d95/rignore-0.7.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f11e61dcaf60273661f233792a6fb144a998a43dd92f9f007e3fda57fbc2178a", size = 1138750 },
{ url = "https://files.pythonhosted.org/packages/a0/8b/821a446169d72280295b76e946d34e4a58aa2ae75e81e317816c9146071e/rignore-0.7.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:844af954b26af2ace2c333b78691acb939e55b3aa7ad5ca0c4cd27085a1088a2", size = 1118562 },
{ url = "https://files.pythonhosted.org/packages/b7/97/68ddb8f52efde41e0079e494eda0ac0168993e9a1a85470d2510575d566f/rignore-0.7.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:999d746d2c345f050b063be57481d3658a4bb561d27685db866c8ebb218d6199", size = 1125516 },
{ url = "https://files.pythonhosted.org/packages/6b/0e/d0de1198a246bd9418945d415f4cf66a4a325ecc5fa6832d3c8648bdf04c/rignore-0.7.2-cp311-cp311-win32.whl", hash = "sha256:b63c880029b3bfb7cfacc43312bae1b73763078ac29f249074fb86e60d2206ac", size = 646549 },
{ url = "https://files.pythonhosted.org/packages/ce/56/c3635d04c29edf7f56ff6b1e30c9d05c4b8a5982f3adea4fc87efe3af260/rignore-0.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:c8c1b971689fbd4cdb88ab63626c45ae22264f8927ab12013764fb8e242b3908", size = 727143 },
{ url = "https://files.pythonhosted.org/packages/19/63/7f16c23743cd7b3a31cf7f02ce9e8b8127873f75250cbdd451e87f1eab4e/rignore-0.7.2-cp311-cp311-win_arm64.whl", hash = "sha256:7de5ce5fd19009656712d70f9b9ab0eeeed2ca126c7080ae7cee87a9256803c0", size = 657622 },
{ url = "https://files.pythonhosted.org/packages/a2/aa/6f21e66910ec1745dec1d1b0ffb97977bfcf76b520c60079c0fca050c702/rignore-0.7.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:2220958d76332fa16aff92b71754ba10601cb2ba66723f87fb64e09d7a8d121a", size = 889758 },
{ url = "https://files.pythonhosted.org/packages/c1/07/20fd5d14677bb34d6ee93f331f4b6bbb593806f8ebda3c0464074b3e8b20/rignore-0.7.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4dd674966c8219b82ea165e3717d629d5a08aad80697489a1bf750c8517f3b9", size = 820405 },
{ url = "https://files.pythonhosted.org/packages/a4/8b/412ab22fba7eeabf7cbd6d5098ee00989dcfcae8b68a8bf3f5c0c21812a0/rignore-0.7.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25323d7d93fa4d93fb2149dbedde911907de933a573b6c475e2d2b248384b42b", size = 901575 },
{ url = "https://files.pythonhosted.org/packages/32/3e/acc817bc5267ffc42ae65f80f12cfa804db6be6ba58c8f3af06a60f5ced7/rignore-0.7.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0b429d97fbe3e2c8180dd10684668d2da624a2f9b2e8cb625dc12cb3c77d1f22", size = 874033 },
{ url = "https://files.pythonhosted.org/packages/05/f8/709846896df0ca1119409339fe9292bdf8f8b142eaee90fc52468844adc5/rignore-0.7.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fbcac80502ca335f38f6bbbacb9b4818622649235d028b0eb01a931b77a38dc2", size = 1176041 },
{ url = "https://files.pythonhosted.org/packages/a4/2f/e4a92d18e4cfcf6f83b82d2928a8f8c5d3a6c8306f4791d89c88cdd80a5f/rignore-0.7.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35a8a220e7a38f672da8ec58126e52ffabfc0d5f833e2047e89f2fa0c2c0cc6a", size = 944453 },
{ url = "https://files.pythonhosted.org/packages/7a/12/ca2d6fc7b68c916fe6d68fc63230f4b60be7a5fc8cebc62962ab342b60c7/rignore-0.7.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e541c1d4717ce5e938748c5302b6ebe63f4eed08bfca68a1861a214a9c8316f2", size = 959266 },
{ url = "https://files.pythonhosted.org/packages/b4/68/97693bbc3fdd65401f44377097040d285f42e6013ba5eb2da8b97fcf2dd4/rignore-0.7.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4a35db5bb7ab3f9a37e131a97bc3388ad3af0fc8a90b9d91799f33afde2a8e15", size = 985338 },
{ url = "https://files.pythonhosted.org/packages/e0/95/b580fdb7666ffe17c52258e48b8d1217be53eaf69f7ad5c2ecc680782836/rignore-0.7.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9cee5d75c8b1bc1450855dde82c6e3ac2d258238410eba710e2326c2e2cf4cce", size = 1081155 },
{ url = "https://files.pythonhosted.org/packages/a1/d7/08d067e0d0011bf18c32833b3f82a119bc191625fb4156db589bb5f3d826/rignore-0.7.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:fd1062ecc16bb8cc32d7ad2e13c04fc3db74919ed8c4d79a9e55b32d50b40c0f", size = 1137958 },
{ url = "https://files.pythonhosted.org/packages/c0/d1/6f3b624e671d2bc24c2a8450d30036a4f387bdae2b556604e0eb67ba3975/rignore-0.7.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e03c758f81f4084d37cd6095c837f24f4fcdc4238d2a921ae880cd0f21c02850", size = 1117831 },
{ url = "https://files.pythonhosted.org/packages/9d/e5/ef8eed0a0f4bab93f470a58430cea4e4b69bb6864bdcb9237276f08a133c/rignore-0.7.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a8d27901bf65db37778990db08cca809cbd4e0be0ea08aef6eb850e4a624b1c1", size = 1125163 },
{ url = "https://files.pythonhosted.org/packages/44/18/11f4af8d56e1941bb8c32749fb15f43b11a855ac4ea0091796c5b289abdf/rignore-0.7.2-cp312-cp312-win32.whl", hash = "sha256:85b511bcd85cc521bfda40ca60a08e35d82c2f4d87a33100f5308cc4e150708e", size = 646153 },
{ url = "https://files.pythonhosted.org/packages/a0/26/b1963edafa3ce974e70ca3fe914ea965c631e21a6cb63de7a87ba78deb89/rignore-0.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:bffd6b885c450ca6d7ab3bb9ff6b1d910a74aec45bb427580c2a91089d2127cb", size = 726157 },
{ url = "https://files.pythonhosted.org/packages/ef/d4/1119b58862bb6e7918c28dd90511f5743a462590eced505caa71f3c7384a/rignore-0.7.2-cp312-cp312-win_arm64.whl", hash = "sha256:036bb1597fab0ebc5082abfaf6b13eaced7769703a492c5772844edc34bc8f76", size = 656322 },
{ url = "https://files.pythonhosted.org/packages/e5/ce/c77d73a611a47b021b1536f7b49fe5593fec0b5e43934166e0c1fcfd1d4c/rignore-0.7.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:2b98b8396f856f302e983664b1e2a13aee8c82d8ce0b34c2548a0e09a7c30d3c", size = 889368 },
{ url = "https://files.pythonhosted.org/packages/11/dc/bbbe0d23051605cd2197626d3a5212f376d5e0143881cdbf6632c8ecb38b/rignore-0.7.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bfdfb906ad0f8f22d2685fb2b6465a078d78ee32e437dab4ab35080a2790c87b", size = 820141 },
{ url = "https://files.pythonhosted.org/packages/5d/62/ee54bc98dc986de7bf8cfddbb62670cbcbbfc21b4c53821421be96d561d0/rignore-0.7.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3eef7c19742af7d3d813917a81af65ed9d7050f49f90fd78986a0243170531a", size = 901513 },
{ url = "https://files.pythonhosted.org/packages/a0/e5/e87a724794d23e1aaf7f9a5b2108fefb64703784e88f1082df36631c424a/rignore-0.7.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a73c14e1a131b17235fac9b148d549e6bd90abb4e9950baeb2df1e09e467bf6d", size = 873815 },
{ url = "https://files.pythonhosted.org/packages/07/02/7a804c2491d9794aef7052a4cdb6343ff6fdee5d68adc6e859f4f46363e8/rignore-0.7.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2927a547bd6934882fc92f55d77b8c4d548655612db502b509e495cbe9ef39eb", size = 1177286 },
{ url = "https://files.pythonhosted.org/packages/4a/6b/0b84972c4442b60d6afb450607708aa74e2b416f403e12541c65a3e49c50/rignore-0.7.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fce3b899a3a891744264edde217a8d3a9fc4e9f542afe1c4b88bfa8544509cca", size = 944310 },
{ url = "https://files.pythonhosted.org/packages/c0/35/abb0816263aaaee399730a701636c81090455203af67601cc409adb8d431/rignore-0.7.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ea5364e7e0a188ee794be4335eaad1df089b8226279b460c98d8b95c11b73d", size = 958713 },
{ url = "https://files.pythonhosted.org/packages/14/70/0573d0bcf3fb27b3960c601027db9e31338c56e3a899e6d1c649b872bb62/rignore-0.7.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a118788ce31693c02629851b4431043d5949c506e15f45d7ccd0cdc3d3e65765", size = 985183 },
{ url = "https://files.pythonhosted.org/packages/72/03/f25ff93e3ede74e8c7908c675ba643ec67fb4fee48a4d8bcc2c2880c53b5/rignore-0.7.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:80c978468562464be9c64de6e086707103a727fec0ec88961d88bca91111f1a9", size = 1080365 },
{ url = "https://files.pythonhosted.org/packages/fd/0c/9a273bf389e0651f118e35f2f4acbe2ed0ceecb570f1ea49475e59d8149e/rignore-0.7.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ea0a073a7b9639be68d8269732630d1ddf55fb72f5e4faa0e1b3d2f46d9e6b48", size = 1137639 },
{ url = "https://files.pythonhosted.org/packages/c9/d5/009ce164e2ef31bc0cf5506337cd5eca495c7b5ea526cb4ccbbbfe8b9928/rignore-0.7.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a15dfd14b5d9118e1e4afbc5e951b1a5ce43185d1605aac5f46ad72a6c53952a", size = 1117566 },
{ url = "https://files.pythonhosted.org/packages/a9/3a/c2aed0787572cc0a0c5afcafb9bbd8827fb676fe89ca3a78cdf62e656f14/rignore-0.7.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bde72ba6474cea23ca9091a66959caaaa915091e472fff95ced1b341d7541300", size = 1124968 },
{ url = "https://files.pythonhosted.org/packages/63/fa/4ab82713918e6a8fc1ef9a609a19baeeb9ddc37e7ba10620045f10689c56/rignore-0.7.2-cp313-cp313-win32.whl", hash = "sha256:011c6ede35ad03c4f84c72c6535033f31c56543830222586e9ef09274b22688a", size = 646108 },
{ url = "https://files.pythonhosted.org/packages/ea/46/c91aac0466158973c8c9deb00ab2bbb870dabc726261dd786246bb62201c/rignore-0.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:26fb0c20f77e24b9dd361cce8c78c7b581fbceab8b2a06e4374c54f5ce63c475", size = 726268 },
{ url = "https://files.pythonhosted.org/packages/3e/41/815c603dff6512ec35ff7ff2b5d8a10f0884203eb71e8d22d5ce3c49bc71/rignore-0.7.2-cp313-cp313-win_arm64.whl", hash = "sha256:4d7d33e36a4f53f1765d3340e126758a1cf232cba9f27d2458f806dad434793e", size = 656198 },
{ url = "https://files.pythonhosted.org/packages/f3/53/b26ad855d846b5426eeb8da22fc47753312b054583cad3a78cbf7375e3e6/rignore-0.7.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d4997bc54ca11f13013e05c271770e1ec20195e4fe21276ea6b91f5c5dced25", size = 820745 },
{ url = "https://files.pythonhosted.org/packages/6f/20/7ebc5949807fb89683d7f3c512d3161d0eb8c01183d0acb569a8f2721eec/rignore-0.7.2-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c5923f3d5481cdd683540ff70c1e9ad1bd369823578e2d49987aedd1c3dedb5", size = 901796 },
{ url = "https://files.pythonhosted.org/packages/be/85/d9166578342e0ef284baece0e843546c1cb4db397d995798a1ec797e502f/rignore-0.7.2-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1cd0a4c1babd64dda268d6a7a5efa998c717e2af0a49717f5f8e9524c92f2595", size = 874141 },
{ url = "https://files.pythonhosted.org/packages/a3/59/83d233b9b787c876d9a2b24efd69a5ad5729f6bb01e0ec753a7e09372ff0/rignore-0.7.2-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e20381b7487479bb75544e6e96141fe20873a8c78c8ed36ceb2ffdbdf9dbfcba", size = 1176316 },
{ url = "https://files.pythonhosted.org/packages/c1/49/852aeab984b7919083e47fe572bcd796bc30653da55b994c1aa2c7b64b8a/rignore-0.7.2-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c82a1f9b5fc264b9286cd2febc8a2e20eaf70e60b436d17393a329e24a8dbae", size = 944566 },
{ url = "https://files.pythonhosted.org/packages/1a/7c/5ae025765f3c66812fc01cdaa4f6ecd809b7f8fa92a39600865d5d9dc538/rignore-0.7.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ef66cd608f5cff2606c4fae81ac6149995c1bb3a7cd442a81c9bc2ee21774c1", size = 958463 },
{ url = "https://files.pythonhosted.org/packages/55/c1/5314352af5633b6d45d910b0fe3b2c5c7473d81a735585fc717f5198e61f/rignore-0.7.2-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dbc864367d79dcdbfd92c463401b637d8760ac8619a8a31210826dd151ff30be", size = 985201 },
{ url = "https://files.pythonhosted.org/packages/1b/a4/4a300a9fb6b2d3a35845c7f51a90ca302b749fcc547e67245232e4c38f98/rignore-0.7.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:51299dcce9edb8a4fafe766ba5f90c02b51a72d2127351cdd62b252fd39e874f", size = 1081867 },
{ url = "https://files.pythonhosted.org/packages/f0/3c/8b074c9f6471588dc898a9d094d35518cb66a3942faeffdae352b2519d1f/rignore-0.7.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9fad9574e1f71f299468d558aa59350600688b05f7ec1d31a01e831ba306d9dd", size = 1138062 },
{ url = "https://files.pythonhosted.org/packages/57/72/d8e0da03c54b282e5fd8f9faf467264d06591c0fff653d243b33aa237e61/rignore-0.7.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:5e12d6c3f420c5362f7ffebca815db298ed0976a98b2bc3e48389bc0a73ffc24", size = 1117732 },
{ url = "https://files.pythonhosted.org/packages/dc/a6/81ce73ccbddfee92d7a1ca655fe9a8f98af19ad4d82283cadb9868e40681/rignore-0.7.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0bb07648a03b7825d70d6ee92e03d7d2220bf9e1eb70a0d93cfddf64b78ce27f", size = 1125026 },
{ url = "https://files.pythonhosted.org/packages/68/d6/85af267bd20130ca58da7ec8d386cccba5a333918a375cca72dc9fb4f3b1/rignore-0.7.2-cp314-cp314-win32.whl", hash = "sha256:95b9a6bc3e83dc42359b276fa795cab81ea33a68662a47b6e7fd201d45187bf7", size = 646386 },
{ url = "https://files.pythonhosted.org/packages/75/25/d85777d2e31d7c42e2d581019b65fd6accfc3645797e011d8a8db3303445/rignore-0.7.2-cp314-cp314-win_amd64.whl", hash = "sha256:afbe88be82ca65debf6f7bc1a9711c4d65dad4156499ded3dfd4e6a7af5f4c78", size = 725700 },
{ url = "https://files.pythonhosted.org/packages/7d/89/e8832494602b2b1f867ca7bf5901a5598fcfc2128510fcef878989cd963f/rignore-0.7.2-cp314-cp314-win_arm64.whl", hash = "sha256:e5429df475e9a17e163352df67c05026e8505da262159c7b9bfa707708bc7b93", size = 656032 },
{ url = "https://files.pythonhosted.org/packages/b5/2c/01237e1ad4fdc6bf370cd193730347f83573bf2094d2a99d58e122865c38/rignore-0.7.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57d65850b7d762a44e4a9b54379fc763042cb0cfbe42ce0d8735ca8bad64e92c", size = 902098 },
{ url = "https://files.pythonhosted.org/packages/37/e4/67fdd0fc28a7c77a0906814faa6e7f8de929897c722adb77944f7cd6338d/rignore-0.7.2-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0553d732ca6413e6c1949d0b793fea807cd5decc38163ec002e7f9faea98f279", size = 874886 },
{ url = "https://files.pythonhosted.org/packages/07/0d/740b9ee613e852728ae251eeecb92c7f40e8e17896e75f373ff66f764c83/rignore-0.7.2-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:760421831764eaaa7d278e10808a01f83da21414750f13084a9715f84050b3b6", size = 1176754 },
{ url = "https://files.pythonhosted.org/packages/6e/9a/bf6891dd85a860a7b9d5f5241db56e1c00a50625996f7638563a5efc2f62/rignore-0.7.2-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3634010a2e9f1307ed7ea6b30d2f5f6e620bb09d4ec4dc44761fa9b9d7d7ec54", size = 945090 },
{ url = "https://files.pythonhosted.org/packages/81/6b/3d2be74922963b932a2cced42ecb1876d717390c02a6b155e73bab9e878a/rignore-0.7.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:399224e352c573946e0d1eb2dc11191a2a00220576fa41493e00da99485b969a", size = 959835 },
{ url = "https://files.pythonhosted.org/packages/24/23/072b6d8c7a7c2479ae2bb8a51b8dba487821baa6d4a82e092e8d7b9b503c/rignore-0.7.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e2776241440dd5e92ea1cae6913d23d18f59e7428676d73534b8a29a21f4ca6a", size = 986706 },
{ url = "https://files.pythonhosted.org/packages/65/dd/93804cb0d26b04bffe7474d4c01f63e6b735d67998dbf9a6e8868666d984/rignore-0.7.2-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:b84e15b1951314592b56421b78eac6324cc870645fdf15fc69f3ab6ae9bd4d71", size = 1082126 },
{ url = "https://files.pythonhosted.org/packages/13/7e/9f0c611b5c5e7c6902b07e3cf98032196c179126fc083d0e166fd8b043a1/rignore-0.7.2-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:a9224806d74f9bce116d622d777b1a052b6e3a60458724f3e66a18a19e764e90", size = 1139089 },
{ url = "https://files.pythonhosted.org/packages/b1/f9/00e1c439804f288510c1ef6171afecd3fdd06d736edd8c5ee4b105d694ad/rignore-0.7.2-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:e8a6a19d11659ea29f8ac16bea2df4826c9c35bdd0d075182123934e45647903", size = 1119255 },
{ url = "https://files.pythonhosted.org/packages/67/2f/e17c92f7b6a38475a966fa684acda8617c50d21c9dc63297a54c568dcd14/rignore-0.7.2-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:68a25d918f5aab4f0af8530e171e5179252d4506bfc89785e5bdc67d8230d0a5", size = 1125901 },
]
[[package]]
name = "safetensors"
version = "0.6.2"
@ -2091,6 +2367,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/bb/a6/a607a737dc1a00b7afe267b9bfde101b8cee2529e197e57471d23137d4e5/sentence_transformers-5.1.2-py3-none-any.whl", hash = "sha256:724ce0ea62200f413f1a5059712aff66495bc4e815a1493f7f9bca242414c333", size = 488009 },
]
[[package]]
name = "sentry-sdk"
version = "2.43.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b3/18/09875b4323b03ca9025bae7e6539797b27e4fc032998a466b4b9c3d24653/sentry_sdk-2.43.0.tar.gz", hash = "sha256:52ed6e251c5d2c084224d73efee56b007ef5c2d408a4a071270e82131d336e20", size = 368953 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/69/31/8228fa962f7fd8814d634e4ebece8780e2cdcfbdf0cd2e14d4a6861a7cd5/sentry_sdk-2.43.0-py2.py3-none-any.whl", hash = "sha256:4aacafcf1756ef066d359ae35030881917160ba7f6fc3ae11e0e58b09edc2d5d", size = 400997 },
]
[[package]]
name = "setuptools"
version = "80.9.0"
@ -2239,6 +2528,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/3a/e2/745aeba88a8513017fbac2fd2f9f07b8a36065e51695f818541eb795ec0c/srsly-2.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:e73712be1634b5e1de6f81c273a7d47fe091ad3c79dc779c03d3416a5c117cee", size = 630634 },
]
[[package]]
name = "starlette"
version = "0.49.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1b/3f/507c21db33b66fb027a332f2cb3abbbe924cc3a79ced12f01ed8645955c9/starlette-0.49.1.tar.gz", hash = "sha256:481a43b71e24ed8c43b11ea02f5353d77840e01480881b8cb5a26b8cae64a8cb", size = 2654703 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/51/da/545b75d420bb23b5d494b0517757b351963e974e79933f01e05c929f20a6/starlette-0.49.1-py3-none-any.whl", hash = "sha256:d92ce9f07e4a3caa3ac13a79523bd18e3bc0042bb8ff2d759a8e7dd0e1859875", size = 74175 },
]
[[package]]
name = "sympy"
version = "1.14.0"
@ -2484,6 +2786,68 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795 },
]
[[package]]
name = "uvicorn"
version = "0.38.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109 },
]
[package.optional-dependencies]
standard = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "httptools" },
{ name = "python-dotenv" },
{ name = "pyyaml" },
{ name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" },
{ name = "watchfiles" },
{ name = "websockets" },
]
[[package]]
name = "uvloop"
version = "0.22.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420 },
{ url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677 },
{ url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819 },
{ url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529 },
{ url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267 },
{ url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105 },
{ url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936 },
{ url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769 },
{ url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413 },
{ url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307 },
{ url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970 },
{ url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343 },
{ url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611 },
{ url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811 },
{ url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562 },
{ url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890 },
{ url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472 },
{ url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051 },
{ url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067 },
{ url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423 },
{ url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437 },
{ url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101 },
{ url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158 },
{ url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360 },
{ url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790 },
{ url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783 },
{ url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548 },
{ url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065 },
{ url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384 },
{ url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730 },
]
[[package]]
name = "wasabi"
version = "1.1.3"
@ -2496,6 +2860,93 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/06/7c/34330a89da55610daa5f245ddce5aab81244321101614751e7537f125133/wasabi-1.1.3-py3-none-any.whl", hash = "sha256:f76e16e8f7e79f8c4c8be49b4024ac725713ab10cd7f19350ad18a8e3f71728c", size = 27880 },
]
[[package]]
name = "watchfiles"
version = "1.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529 },
{ url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384 },
{ url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789 },
{ url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521 },
{ url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722 },
{ url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088 },
{ url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923 },
{ url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080 },
{ url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432 },
{ url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046 },
{ url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473 },
{ url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598 },
{ url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210 },
{ url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745 },
{ url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769 },
{ url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374 },
{ url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485 },
{ url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813 },
{ url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816 },
{ url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186 },
{ url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812 },
{ url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196 },
{ url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657 },
{ url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042 },
{ url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410 },
{ url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209 },
{ url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321 },
{ url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783 },
{ url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279 },
{ url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405 },
{ url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976 },
{ url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506 },
{ url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936 },
{ url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147 },
{ url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007 },
{ url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280 },
{ url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056 },
{ url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162 },
{ url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909 },
{ url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389 },
{ url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964 },
{ url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114 },
{ url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264 },
{ url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877 },
{ url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176 },
{ url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577 },
{ url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425 },
{ url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826 },
{ url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208 },
{ url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315 },
{ url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869 },
{ url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919 },
{ url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845 },
{ url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027 },
{ url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615 },
{ url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836 },
{ url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099 },
{ url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626 },
{ url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519 },
{ url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078 },
{ url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664 },
{ url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154 },
{ url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820 },
{ url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510 },
{ url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408 },
{ url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968 },
{ url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096 },
{ url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040 },
{ url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847 },
{ url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072 },
{ url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104 },
{ url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112 },
{ url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250 },
{ url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117 },
{ url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493 },
{ url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546 },
]
[[package]]
name = "weasel"
version = "0.4.1"
@ -2516,6 +2967,60 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/87/abd57374044e1f627f0a905ac33c1a7daab35a3a815abfea4e1bafd3fdb1/weasel-0.4.1-py3-none-any.whl", hash = "sha256:24140a090ea1ac512a2b2f479cc64192fd1d527a7f3627671268d08ed5ac418c", size = 50270 },
]
[[package]]
name = "websockets"
version = "15.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423 },
{ url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082 },
{ url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330 },
{ url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878 },
{ url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883 },
{ url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252 },
{ url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521 },
{ url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958 },
{ url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918 },
{ url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388 },
{ url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828 },
{ url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437 },
{ url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096 },
{ url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332 },
{ url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152 },
{ url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096 },
{ url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523 },
{ url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790 },
{ url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165 },
{ url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160 },
{ url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395 },
{ url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841 },
{ url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440 },
{ url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098 },
{ url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329 },
{ url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111 },
{ url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054 },
{ url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496 },
{ url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829 },
{ url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217 },
{ url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195 },
{ url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393 },
{ url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837 },
{ url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 },
]
[[package]]
name = "werkzeug"
version = "3.1.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498 },
]
[[package]]
name = "wrapt"
version = "2.0.0"

View file

@ -1,593 +0,0 @@
"""
Interactive HTML graph visualization of memory system.
Uses Cytoscape.js to create a performant, interactive network graph that can be
explored in the browser. Shows all memory units and their links with weights.
"""
import psycopg2
from dotenv import load_dotenv
import os
import json
load_dotenv()
def create_interactive_graph():
"""Create an interactive HTML graph visualization using Cytoscape.js."""
# Connect to database
conn = psycopg2.connect(os.getenv('DATABASE_URL'))
cursor = conn.cursor()
# Get all memory units (no agent_id filter)
cursor.execute("""
SELECT id, text, event_date, context
FROM memory_units
ORDER BY event_date
""")
units = cursor.fetchall()
# Get all links with weights (no agent_id filter)
cursor.execute("""
SELECT
ml.from_unit_id,
ml.to_unit_id,
ml.link_type,
ml.weight,
e.canonical_name as entity_name
FROM memory_links ml
LEFT JOIN entities e ON ml.entity_id = e.id
ORDER BY ml.link_type, ml.weight DESC
""")
links = cursor.fetchall()
# Get entity information (no agent_id filter)
cursor.execute("""
SELECT ue.unit_id, e.canonical_name, e.entity_type
FROM unit_entities ue
JOIN entities e ON ue.entity_id = e.id
ORDER BY ue.unit_id
""")
unit_entities = cursor.fetchall()
cursor.close()
conn.close()
# Build entity mapping
entity_map = {}
for unit_id, entity_name, entity_type in unit_entities:
if unit_id not in entity_map:
entity_map[unit_id] = []
entity_map[unit_id].append(f"{entity_name} ({entity_type})")
# Build Cytoscape.js graph data
cy_nodes = []
cy_edges = []
# Add nodes
for unit_id, text, event_date, context in units:
entities = entity_map.get(unit_id, [])
entity_count = len(entities)
# Color by entity count
if entity_count == 0:
color = "#e0e0e0"
elif entity_count == 1:
color = "#90caf9"
else:
color = "#42a5f5"
cy_nodes.append({
"data": {
"id": str(unit_id),
"label": text[:50] + "..." if len(text) > 50 else text,
"text": text,
"context": context,
"date": str(event_date.date()),
"entities": ", ".join(entities) if entities else "None",
"color": color
}
})
# Add edges
for from_id, to_id, link_type, weight, entity_name in links:
# Set color based on link type
if link_type == 'temporal':
color = "#00bcd4"
line_style = "dashed"
elif link_type == 'semantic':
color = "#ff69b4"
line_style = "solid"
elif link_type == 'entity':
color = "#ffd700"
line_style = "solid"
else:
color = "#999999"
line_style = "solid"
cy_edges.append({
"data": {
"id": f"{from_id}-{to_id}-{link_type}",
"source": str(from_id),
"target": str(to_id),
"weight": weight,
"linkType": link_type,
"entityName": entity_name or "",
"color": color,
"lineStyle": line_style
}
})
graph_data = {"nodes": cy_nodes, "edges": cy_edges}
# Build table rows for table view
table_rows = []
for unit_id, text, event_date, context in units:
entities = entity_map.get(unit_id, [])
entity_str = ", ".join(entities) if entities else "None"
table_rows.append(f"""
<tr>
<td style="padding: 8px; border: 1px solid #ddd;">{str(unit_id)[:8]}...</td>
<td style="padding: 8px; border: 1px solid #ddd;">{text}</td>
<td style="padding: 8px; border: 1px solid #ddd;">{context}</td>
<td style="padding: 8px; border: 1px solid #ddd;">{event_date.date()}</td>
<td style="padding: 8px; border: 1px solid #ddd;">{entity_str}</td>
</tr>
""")
# Generate HTML with Cytoscape.js
html_content = f"""
<!DOCTYPE html>
<html>
<head>
<title>Memory Graph - Interactive Visualization</title>
<meta charset="utf-8">
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.28.1/cytoscape.min.js"></script>
<style>
body {{
font-family: Tahoma, sans-serif;
margin: 0;
padding: 0;
background: #f5f5f5;
}}
.tab-container {{
background: white;
}}
.tab-buttons {{
background: #f0f0f0;
border-bottom: 2px solid #333;
padding: 0;
margin: 0;
}}
.tab-button {{
background: #e0e0e0;
border: none;
padding: 12px 24px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
border-top: 2px solid transparent;
border-left: 2px solid transparent;
border-right: 2px solid transparent;
transition: background 0.2s;
}}
.tab-button:hover {{
background: #d0d0d0;
}}
.tab-button.active {{
background: white;
border-top: 2px solid #333;
border-left: 2px solid #333;
border-right: 2px solid #333;
border-bottom: 2px solid white;
margin-bottom: -2px;
}}
.tab-content {{
display: none;
background: white;
}}
.tab-content.active {{
display: block;
}}
#cy {{
width: 100%;
height: 800px;
background: #ffffff;
}}
#graph-tab {{
position: relative;
}}
#table-tab {{
padding: 20px;
}}
.legend {{
position: absolute;
top: 20px;
left: 20px;
background: white;
padding: 15px;
border: 2px solid #333;
border-radius: 8px;
box-shadow: 2px 2px 8px rgba(0,0,0,0.3);
z-index: 1000;
max-width: 250px;
}}
.legend h3 {{
margin-top: 0;
border-bottom: 2px solid #333;
padding-bottom: 5px;
}}
.legend-item {{
margin: 8px 0;
display: flex;
align-items: center;
}}
.legend-line {{
width: 30px;
height: 2px;
margin-right: 10px;
}}
.legend-node {{
width: 20px;
height: 20px;
margin-right: 10px;
border: 1px solid #999;
border-radius: 3px;
}}
#table-filter {{
width: 100%;
max-width: 600px;
padding: 10px;
margin-bottom: 15px;
border: 2px solid #ccc;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
}}
#memory-table {{
width: 100%;
border-collapse: collapse;
font-size: 13px;
max-width: 1400px;
}}
#memory-table th {{
padding: 10px;
text-align: left;
border: 1px solid #ddd;
background: #f0f0f0;
}}
#memory-table td {{
padding: 8px;
border: 1px solid #ddd;
}}
.tooltip {{
position: absolute;
background: white;
border: 2px solid #333;
border-radius: 4px;
padding: 10px;
box-shadow: 2px 2px 8px rgba(0,0,0,0.3);
max-width: 300px;
font-size: 12px;
pointer-events: none;
z-index: 9999;
}}
</style>
</head>
<body>
<div class="tab-container">
<div class="tab-buttons">
<button class="tab-button active" onclick="switchTab('graph')">Graph View</button>
<button class="tab-button" onclick="switchTab('table')">Table View</button>
</div>
<div id="graph-tab" class="tab-content active">
<div style="padding: 15px; background: #f9f9f9; border-bottom: 2px solid #333;">
<div style="display: flex; gap: 15px; align-items: center; flex-wrap: wrap;">
<div>
<label style="font-weight: bold; margin-right: 5px;">Limit nodes:</label>
<input type="number" id="node-limit" value="50" min="10" max="1000" step="10"
style="width: 80px; padding: 5px; border: 1px solid #ccc; border-radius: 4px;">
</div>
<div>
<label style="font-weight: bold; margin-right: 5px;">Layout:</label>
<select id="layout-select" style="padding: 5px; border: 1px solid #ccc; border-radius: 4px;">
<option value="circle">Circle (fast)</option>
<option value="grid">Grid (fast)</option>
<option value="cose">Force-directed (slow)</option>
</select>
</div>
<button onclick="reloadGraph()" style="padding: 6px 15px; background: #42a5f5; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;">
Apply
</button>
<span id="node-count" style="color: #666; font-size: 14px;"></span>
</div>
</div>
<div id="cy"></div>
<div class="legend">
<h3>Legend</h3>
<h4 style="margin: 10px 0 5px 0;">Link Types:</h4>
<div class="legend-item">
<div class="legend-line" style="background: #00bcd4; border-top: 1px dashed #00bcd4;"></div>
<span><b>Temporal</b></span>
</div>
<div class="legend-item">
<div class="legend-line" style="background: #ff69b4;"></div>
<span><b>Semantic</b></span>
</div>
<div class="legend-item">
<div class="legend-line" style="background: #ffd700;"></div>
<span><b>Entity</b></span>
</div>
<h4 style="margin: 15px 0 5px 0;">Nodes:</h4>
<div class="legend-item">
<div class="legend-node" style="background: #e0e0e0;"></div>
<span>No entities</span>
</div>
<div class="legend-item">
<div class="legend-node" style="background: #90caf9;"></div>
<span>1 entity</span>
</div>
<div class="legend-item">
<div class="legend-node" style="background: #42a5f5;"></div>
<span>2+ entities</span>
</div>
</div>
</div>
<div id="table-tab" class="tab-content">
<h2>Memory Units ({len(units)})</h2>
<input type="text" id="table-filter" placeholder="Filter by text, context, or entities...">
<div style="overflow-x: auto;">
<table id="memory-table">
<thead>
<tr>
<th>ID</th>
<th>Text</th>
<th>Context</th>
<th>Date</th>
<th>Entities</th>
</tr>
</thead>
<tbody>
{''.join(table_rows)}
</tbody>
</table>
</div>
</div>
</div>
<script>
// Graph data
const allGraphData = {json.dumps(graph_data)};
let cy = null;
// Initialize graph with filtering
function initGraph(nodeLimit, layoutName) {{
// Filter nodes to limit
const limitedNodes = allGraphData.nodes.slice(0, nodeLimit);
const nodeIds = new Set(limitedNodes.map(n => n.data.id));
// Filter edges to only include those between visible nodes
const limitedEdges = allGraphData.edges.filter(e =>
nodeIds.has(e.data.source) && nodeIds.has(e.data.target)
);
// Update count display
document.getElementById('node-count').textContent =
`Showing ${{limitedNodes.length}} of ${{allGraphData.nodes.length}} nodes`;
// Destroy existing graph if any
if (cy) {{
cy.destroy();
}}
// Layout configurations
const layouts = {{
'circle': {{
name: 'circle',
animate: false,
radius: 300,
spacingFactor: 1.5
}},
'grid': {{
name: 'grid',
animate: false,
rows: Math.ceil(Math.sqrt(limitedNodes.length)),
cols: Math.ceil(Math.sqrt(limitedNodes.length)),
spacingFactor: 2
}},
'cose': {{
name: 'cose',
animate: false,
nodeRepulsion: 15000,
idealEdgeLength: 150,
edgeElasticity: 100,
nestingFactor: 1.2,
gravity: 1,
numIter: 1000,
initialTemp: 200,
coolingFactor: 0.95,
minTemp: 1.0
}}
}};
// Initialize Cytoscape
cy = cytoscape({{
container: document.getElementById('cy'),
elements: [
...limitedNodes.map(n => ({{ data: n.data }})),
...limitedEdges.map(e => ({{ data: e.data }}))
],
style: [
{{
selector: 'node',
style: {{
'background-color': 'data(color)',
'label': 'data(label)',
'text-valign': 'center',
'text-halign': 'center',
'font-size': '10px',
'font-weight': 'bold',
'text-wrap': 'wrap',
'text-max-width': '100px',
'width': 40,
'height': 40,
'border-width': 2,
'border-color': '#333'
}}
}},
{{
selector: 'edge',
style: {{
'width': 1,
'line-color': 'data(color)',
'line-style': 'data(lineStyle)',
'target-arrow-shape': 'triangle',
'target-arrow-color': 'data(color)',
'curve-style': 'bezier',
'opacity': 0.7
}}
}},
{{
selector: 'node:selected',
style: {{
'border-width': 4,
'border-color': '#000'
}}
}}
],
layout: layouts[layoutName] || layouts['circle']
}});
// Simple tooltip on hover
let tooltip = null;
cy.on('mouseover', 'node', function(evt) {{
const node = evt.target;
const data = node.data();
const renderedPosition = node.renderedPosition();
// Create tooltip
tooltip = document.createElement('div');
tooltip.className = 'tooltip';
tooltip.innerHTML = `
<b>Text:</b> ${{data.text}}<br>
<b>Context:</b> ${{data.context}}<br>
<b>Date:</b> ${{data.date}}<br>
<b>Entities:</b> ${{data.entities}}
`;
tooltip.style.left = renderedPosition.x + 20 + 'px';
tooltip.style.top = renderedPosition.y + 'px';
document.body.appendChild(tooltip);
}});
cy.on('mouseout', 'node', function(evt) {{
if (tooltip) {{
tooltip.remove();
tooltip = null;
}}
}});
}}
// Reload graph with current settings
function reloadGraph() {{
const nodeLimit = parseInt(document.getElementById('node-limit').value) || 50;
const layoutName = document.getElementById('layout-select').value;
initGraph(nodeLimit, layoutName);
}}
// Initialize with default settings (50 nodes, circle layout)
initGraph(50, 'circle');
// Tab switching
function switchTab(tabName) {{
document.querySelectorAll('.tab-content').forEach(tab => {{
tab.classList.remove('active');
}});
document.querySelectorAll('.tab-button').forEach(btn => {{
btn.classList.remove('active');
}});
if (tabName === 'graph') {{
document.getElementById('graph-tab').classList.add('active');
document.querySelectorAll('.tab-button')[0].classList.add('active');
cy.resize(); // Resize graph when switching to it
}} else if (tabName === 'table') {{
document.getElementById('table-tab').classList.add('active');
document.querySelectorAll('.tab-button')[1].classList.add('active');
}}
}}
// Table filtering
document.getElementById('table-filter').addEventListener('input', function() {{
const filterValue = this.value.toLowerCase();
const rows = document.querySelectorAll('#memory-table tbody tr');
rows.forEach(row => {{
const text = row.textContent.toLowerCase();
if (text.includes(filterValue)) {{
row.style.display = '';
}} else {{
row.style.display = 'none';
}}
}});
}});
</script>
</body>
</html>
"""
# Write HTML file
output_file = "memory_graph_interactive.html"
with open(output_file, 'w', encoding='utf-8') as f:
f.write(html_content)
# Print summary
print(f"\n{'='*80}")
print("INTERACTIVE GRAPH GENERATED (Cytoscape.js)")
print(f"{'='*80}")
print(f"\nFile: {output_file}")
print(f"Units: {len(units)}")
print(f"Links: {len(links)}")
print("\nFeatures:")
print(" • Tab 1: Graph View - Fast interactive network (Cytoscape.js)")
print(" - Limit nodes (default: 50) for better performance")
print(" - Choose layout: Circle (fast), Grid (fast), or Force-directed")
print(" - Drag nodes, zoom, pan")
print(" - Hover for details")
print(" • Tab 2: Table View - Searchable memory units")
print(" - Filter by text, context, or entities")
print(" - Case-insensitive search")
print(" - Shows ALL nodes")
print(f"\n{'='*80}")
print(f"✓ Open {output_file} in your browser to explore!")
print(f" TIP: Start with 50 nodes and Circle layout for best performance")
print(f"{'='*80}\n")
if __name__ == "__main__":
create_interactive_graph()

View file

@ -1,189 +0,0 @@
function neighbourhoodHighlight(params) {
// console.log("in nieghbourhoodhighlight");
allNodes = nodes.get({ returnType: "Object" });
// originalNodes = JSON.parse(JSON.stringify(allNodes));
// if something is selected:
if (params.nodes.length > 0) {
highlightActive = true;
var i, j;
var selectedNode = params.nodes[0];
var degrees = 2;
// mark all nodes as hard to read.
for (let nodeId in allNodes) {
// nodeColors[nodeId] = allNodes[nodeId].color;
allNodes[nodeId].color = "rgba(200,200,200,0.5)";
if (allNodes[nodeId].hiddenLabel === undefined) {
allNodes[nodeId].hiddenLabel = allNodes[nodeId].label;
allNodes[nodeId].label = undefined;
}
}
var connectedNodes = network.getConnectedNodes(selectedNode);
var allConnectedNodes = [];
// get the second degree nodes
for (i = 1; i < degrees; i++) {
for (j = 0; j < connectedNodes.length; j++) {
allConnectedNodes = allConnectedNodes.concat(
network.getConnectedNodes(connectedNodes[j])
);
}
}
// all second degree nodes get a different color and their label back
for (i = 0; i < allConnectedNodes.length; i++) {
// allNodes[allConnectedNodes[i]].color = "pink";
allNodes[allConnectedNodes[i]].color = "rgba(150,150,150,0.75)";
if (allNodes[allConnectedNodes[i]].hiddenLabel !== undefined) {
allNodes[allConnectedNodes[i]].label =
allNodes[allConnectedNodes[i]].hiddenLabel;
allNodes[allConnectedNodes[i]].hiddenLabel = undefined;
}
}
// all first degree nodes get their own color and their label back
for (i = 0; i < connectedNodes.length; i++) {
// allNodes[connectedNodes[i]].color = undefined;
allNodes[connectedNodes[i]].color = nodeColors[connectedNodes[i]];
if (allNodes[connectedNodes[i]].hiddenLabel !== undefined) {
allNodes[connectedNodes[i]].label =
allNodes[connectedNodes[i]].hiddenLabel;
allNodes[connectedNodes[i]].hiddenLabel = undefined;
}
}
// the main node gets its own color and its label back.
// allNodes[selectedNode].color = undefined;
allNodes[selectedNode].color = nodeColors[selectedNode];
if (allNodes[selectedNode].hiddenLabel !== undefined) {
allNodes[selectedNode].label = allNodes[selectedNode].hiddenLabel;
allNodes[selectedNode].hiddenLabel = undefined;
}
} else if (highlightActive === true) {
// console.log("highlightActive was true");
// reset all nodes
for (let nodeId in allNodes) {
// allNodes[nodeId].color = "purple";
allNodes[nodeId].color = nodeColors[nodeId];
// delete allNodes[nodeId].color;
if (allNodes[nodeId].hiddenLabel !== undefined) {
allNodes[nodeId].label = allNodes[nodeId].hiddenLabel;
allNodes[nodeId].hiddenLabel = undefined;
}
}
highlightActive = false;
}
// transform the object into an array
var updateArray = [];
if (params.nodes.length > 0) {
for (let nodeId in allNodes) {
if (allNodes.hasOwnProperty(nodeId)) {
// console.log(allNodes[nodeId]);
updateArray.push(allNodes[nodeId]);
}
}
nodes.update(updateArray);
} else {
// console.log("Nothing was selected");
for (let nodeId in allNodes) {
if (allNodes.hasOwnProperty(nodeId)) {
// console.log(allNodes[nodeId]);
// allNodes[nodeId].color = {};
updateArray.push(allNodes[nodeId]);
}
}
nodes.update(updateArray);
}
}
function filterHighlight(params) {
allNodes = nodes.get({ returnType: "Object" });
// if something is selected:
if (params.nodes.length > 0) {
filterActive = true;
let selectedNodes = params.nodes;
// hiding all nodes and saving the label
for (let nodeId in allNodes) {
allNodes[nodeId].hidden = true;
if (allNodes[nodeId].savedLabel === undefined) {
allNodes[nodeId].savedLabel = allNodes[nodeId].label;
allNodes[nodeId].label = undefined;
}
}
for (let i=0; i < selectedNodes.length; i++) {
allNodes[selectedNodes[i]].hidden = false;
if (allNodes[selectedNodes[i]].savedLabel !== undefined) {
allNodes[selectedNodes[i]].label = allNodes[selectedNodes[i]].savedLabel;
allNodes[selectedNodes[i]].savedLabel = undefined;
}
}
} else if (filterActive === true) {
// reset all nodes
for (let nodeId in allNodes) {
allNodes[nodeId].hidden = false;
if (allNodes[nodeId].savedLabel !== undefined) {
allNodes[nodeId].label = allNodes[nodeId].savedLabel;
allNodes[nodeId].savedLabel = undefined;
}
}
filterActive = false;
}
// transform the object into an array
var updateArray = [];
if (params.nodes.length > 0) {
for (let nodeId in allNodes) {
if (allNodes.hasOwnProperty(nodeId)) {
updateArray.push(allNodes[nodeId]);
}
}
nodes.update(updateArray);
} else {
for (let nodeId in allNodes) {
if (allNodes.hasOwnProperty(nodeId)) {
updateArray.push(allNodes[nodeId]);
}
}
nodes.update(updateArray);
}
}
function selectNode(nodes) {
network.selectNodes(nodes);
neighbourhoodHighlight({ nodes: nodes });
return nodes;
}
function selectNodes(nodes) {
network.selectNodes(nodes);
filterHighlight({nodes: nodes});
return nodes;
}
function highlightFilter(filter) {
let selectedNodes = []
let selectedProp = filter['property']
if (filter['item'] === 'node') {
let allNodes = nodes.get({ returnType: "Object" });
for (let nodeId in allNodes) {
if (allNodes[nodeId][selectedProp] && filter['value'].includes((allNodes[nodeId][selectedProp]).toString())) {
selectedNodes.push(nodeId)
}
}
}
else if (filter['item'] === 'edge'){
let allEdges = edges.get({returnType: 'object'});
// check if the selected property exists for selected edge and select the nodes connected to the edge
for (let edge in allEdges) {
if (allEdges[edge][selectedProp] && filter['value'].includes((allEdges[edge][selectedProp]).toString())) {
selectedNodes.push(allEdges[edge]['from'])
selectedNodes.push(allEdges[edge]['to'])
}
}
}
selectNodes(selectedNodes)
}

View file

@ -1,356 +0,0 @@
/**
* Tom Select v2.0.0-rc.4
* Licensed under the Apache License, Version 2.0 (the "License");
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).TomSelect=t()}(this,(function(){"use strict"
function e(e,t){e.split(/\s+/).forEach((e=>{t(e)}))}class t{constructor(){this._events={}}on(t,i){e(t,(e=>{this._events[e]=this._events[e]||[],this._events[e].push(i)}))}off(t,i){var s=arguments.length
0!==s?e(t,(e=>{if(1===s)return delete this._events[e]
e in this._events!=!1&&this._events[e].splice(this._events[e].indexOf(i),1)})):this._events={}}trigger(t,...i){var s=this
e(t,(e=>{if(e in s._events!=!1)for(let t of s._events[e])t.apply(s,i)}))}}var i
const s="[̀-ͯ·ʾ]",n=new RegExp(s,"g")
var o
const r={"æ":"ae","ⱥ":"a","ø":"o"},l=new RegExp(Object.keys(r).join("|"),"g"),a=[[67,67],[160,160],[192,438],[452,652],[961,961],[1019,1019],[1083,1083],[1281,1289],[1984,1984],[5095,5095],[7429,7441],[7545,7549],[7680,7935],[8580,8580],[9398,9449],[11360,11391],[42792,42793],[42802,42851],[42873,42897],[42912,42922],[64256,64260],[65313,65338],[65345,65370]],c=e=>e.normalize("NFKD").replace(n,"").toLowerCase().replace(l,(function(e){return r[e]})),d=(e,t="|")=>{if(1==e.length)return e[0]
var i=1
return e.forEach((e=>{i=Math.max(i,e.length)})),1==i?"["+e.join("")+"]":"(?:"+e.join(t)+")"},p=e=>{if(1===e.length)return[[e]]
var t=[]
return p(e.substring(1)).forEach((function(i){var s=i.slice(0)
s[0]=e.charAt(0)+s[0],t.push(s),(s=i.slice(0)).unshift(e.charAt(0)),t.push(s)})),t},u=e=>{void 0===o&&(o=(()=>{var e={}
a.forEach((t=>{for(let s=t[0];s<=t[1];s++){let t=String.fromCharCode(s),n=c(t)
if(n!=t.toLowerCase()){n in e||(e[n]=[n])
var i=new RegExp(d(e[n]),"iu")
t.match(i)||e[n].push(t)}}}))
var t=Object.keys(e)
t=t.sort(((e,t)=>t.length-e.length)),i=new RegExp("("+d(t)+"[̀-ͯ·ʾ]*)","g")
var s={}
return t.sort(((e,t)=>e.length-t.length)).forEach((t=>{var i=p(t).map((t=>(t=t.map((t=>e.hasOwnProperty(t)?d(e[t]):t)),d(t,""))))
s[t]=d(i)})),s})())
return e.normalize("NFKD").toLowerCase().split(i).map((e=>{if(""==e)return""
const t=c(e)
if(o.hasOwnProperty(t))return o[t]
const i=e.normalize("NFC")
return i!=e?d([e,i]):e})).join("")},h=(e,t)=>{if(e)return e[t]},g=(e,t)=>{if(e){for(var i,s=t.split(".");(i=s.shift())&&(e=e[i]););return e}},f=(e,t,i)=>{var s,n
return e?-1===(n=(e+="").search(t.regex))?0:(s=t.string.length/e.length,0===n&&(s+=.5),s*i):0},v=e=>(e+"").replace(/([\$\(-\+\.\?\[-\^\{-\}])/g,"\\$1"),m=(e,t)=>{var i=e[t]
if("function"==typeof i)return i
i&&!Array.isArray(i)&&(e[t]=[i])},y=(e,t)=>{if(Array.isArray(e))e.forEach(t)
else for(var i in e)e.hasOwnProperty(i)&&t(e[i],i)},O=(e,t)=>"number"==typeof e&&"number"==typeof t?e>t?1:e<t?-1:0:(e=c(e+"").toLowerCase())>(t=c(t+"").toLowerCase())?1:t>e?-1:0
class b{constructor(e,t){this.items=e,this.settings=t||{diacritics:!0}}tokenize(e,t,i){if(!e||!e.length)return[]
const s=[],n=e.split(/\s+/)
var o
return i&&(o=new RegExp("^("+Object.keys(i).map(v).join("|")+"):(.*)$")),n.forEach((e=>{let i,n=null,r=null
o&&(i=e.match(o))&&(n=i[1],e=i[2]),e.length>0&&(r=v(e),this.settings.diacritics&&(r=u(r)),t&&(r="\\b"+r)),s.push({string:e,regex:r?new RegExp(r,"iu"):null,field:n})})),s}getScoreFunction(e,t){var i=this.prepareSearch(e,t)
return this._getScoreFunction(i)}_getScoreFunction(e){const t=e.tokens,i=t.length
if(!i)return function(){return 0}
const s=e.options.fields,n=e.weights,o=s.length,r=e.getAttrFn
if(!o)return function(){return 1}
const l=1===o?function(e,t){const i=s[0].field
return f(r(t,i),e,n[i])}:function(e,t){var i=0
if(e.field){const s=r(t,e.field)
!e.regex&&s?i+=1/o:i+=f(s,e,1)}else y(n,((s,n)=>{i+=f(r(t,n),e,s)}))
return i/o}
return 1===i?function(e){return l(t[0],e)}:"and"===e.options.conjunction?function(e){for(var s,n=0,o=0;n<i;n++){if((s=l(t[n],e))<=0)return 0
o+=s}return o/i}:function(e){var s=0
return y(t,(t=>{s+=l(t,e)})),s/i}}getSortFunction(e,t){var i=this.prepareSearch(e,t)
return this._getSortFunction(i)}_getSortFunction(e){var t,i,s
const n=this,o=e.options,r=!e.query&&o.sort_empty?o.sort_empty:o.sort,l=[],a=[]
if("function"==typeof r)return r.bind(this)
const c=function(t,i){return"$score"===t?i.score:e.getAttrFn(n.items[i.id],t)}
if(r)for(t=0,i=r.length;t<i;t++)(e.query||"$score"!==r[t].field)&&l.push(r[t])
if(e.query){for(s=!0,t=0,i=l.length;t<i;t++)if("$score"===l[t].field){s=!1
break}s&&l.unshift({field:"$score",direction:"desc"})}else for(t=0,i=l.length;t<i;t++)if("$score"===l[t].field){l.splice(t,1)
break}for(t=0,i=l.length;t<i;t++)a.push("desc"===l[t].direction?-1:1)
const d=l.length
if(d){if(1===d){const e=l[0].field,t=a[0]
return function(i,s){return t*O(c(e,i),c(e,s))}}return function(e,t){var i,s,n
for(i=0;i<d;i++)if(n=l[i].field,s=a[i]*O(c(n,e),c(n,t)))return s
return 0}}return null}prepareSearch(e,t){const i={}
var s=Object.assign({},t)
if(m(s,"sort"),m(s,"sort_empty"),s.fields){m(s,"fields")
const e=[]
s.fields.forEach((t=>{"string"==typeof t&&(t={field:t,weight:1}),e.push(t),i[t.field]="weight"in t?t.weight:1})),s.fields=e}return{options:s,query:e.toLowerCase().trim(),tokens:this.tokenize(e,s.respect_word_boundaries,i),total:0,items:[],weights:i,getAttrFn:s.nesting?g:h}}search(e,t){var i,s,n=this
s=this.prepareSearch(e,t),t=s.options,e=s.query
const o=t.score||n._getScoreFunction(s)
e.length?y(n.items,((e,n)=>{i=o(e),(!1===t.filter||i>0)&&s.items.push({score:i,id:n})})):y(n.items,((e,t)=>{s.items.push({score:1,id:t})}))
const r=n._getSortFunction(s)
return r&&s.items.sort(r),s.total=s.items.length,"number"==typeof t.limit&&(s.items=s.items.slice(0,t.limit)),s}}const w=e=>{if(e.jquery)return e[0]
if(e instanceof HTMLElement)return e
if(e.indexOf("<")>-1){let t=document.createElement("div")
return t.innerHTML=e.trim(),t.firstChild}return document.querySelector(e)},_=(e,t)=>{var i=document.createEvent("HTMLEvents")
i.initEvent(t,!0,!1),e.dispatchEvent(i)},I=(e,t)=>{Object.assign(e.style,t)},C=(e,...t)=>{var i=A(t);(e=x(e)).map((e=>{i.map((t=>{e.classList.add(t)}))}))},S=(e,...t)=>{var i=A(t);(e=x(e)).map((e=>{i.map((t=>{e.classList.remove(t)}))}))},A=e=>{var t=[]
return y(e,(e=>{"string"==typeof e&&(e=e.trim().split(/[\11\12\14\15\40]/)),Array.isArray(e)&&(t=t.concat(e))})),t.filter(Boolean)},x=e=>(Array.isArray(e)||(e=[e]),e),k=(e,t,i)=>{if(!i||i.contains(e))for(;e&&e.matches;){if(e.matches(t))return e
e=e.parentNode}},F=(e,t=0)=>t>0?e[e.length-1]:e[0],L=(e,t)=>{if(!e)return-1
t=t||e.nodeName
for(var i=0;e=e.previousElementSibling;)e.matches(t)&&i++
return i},P=(e,t)=>{y(t,((t,i)=>{null==t?e.removeAttribute(i):e.setAttribute(i,""+t)}))},E=(e,t)=>{e.parentNode&&e.parentNode.replaceChild(t,e)},T=(e,t)=>{if(null===t)return
if("string"==typeof t){if(!t.length)return
t=new RegExp(t,"i")}const i=e=>3===e.nodeType?(e=>{var i=e.data.match(t)
if(i&&e.data.length>0){var s=document.createElement("span")
s.className="highlight"
var n=e.splitText(i.index)
n.splitText(i[0].length)
var o=n.cloneNode(!0)
return s.appendChild(o),E(n,s),1}return 0})(e):((e=>{if(1===e.nodeType&&e.childNodes&&!/(script|style)/i.test(e.tagName)&&("highlight"!==e.className||"SPAN"!==e.tagName))for(var t=0;t<e.childNodes.length;++t)t+=i(e.childNodes[t])})(e),0)
i(e)},V="undefined"!=typeof navigator&&/Mac/.test(navigator.userAgent)?"metaKey":"ctrlKey"
var j={options:[],optgroups:[],plugins:[],delimiter:",",splitOn:null,persist:!0,diacritics:!0,create:null,createOnBlur:!1,createFilter:null,highlight:!0,openOnFocus:!0,shouldOpen:null,maxOptions:50,maxItems:null,hideSelected:null,duplicates:!1,addPrecedence:!1,selectOnTab:!1,preload:null,allowEmptyOption:!1,loadThrottle:300,loadingClass:"loading",dataAttr:null,optgroupField:"optgroup",valueField:"value",labelField:"text",disabledField:"disabled",optgroupLabelField:"label",optgroupValueField:"value",lockOptgroupOrder:!1,sortField:"$order",searchField:["text"],searchConjunction:"and",mode:null,wrapperClass:"ts-wrapper",controlClass:"ts-control",dropdownClass:"ts-dropdown",dropdownContentClass:"ts-dropdown-content",itemClass:"item",optionClass:"option",dropdownParent:null,copyClassesToDropdown:!1,placeholder:null,hidePlaceholder:null,shouldLoad:function(e){return e.length>0},render:{}}
const q=e=>null==e?null:D(e),D=e=>"boolean"==typeof e?e?"1":"0":e+"",N=e=>(e+"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"),z=(e,t)=>{var i
return function(s,n){var o=this
i&&(o.loading=Math.max(o.loading-1,0),clearTimeout(i)),i=setTimeout((function(){i=null,o.loadedSearches[s]=!0,e.call(o,s,n)}),t)}},R=(e,t,i)=>{var s,n=e.trigger,o={}
for(s in e.trigger=function(){var i=arguments[0]
if(-1===t.indexOf(i))return n.apply(e,arguments)
o[i]=arguments},i.apply(e,[]),e.trigger=n,o)n.apply(e,o[s])},H=(e,t=!1)=>{e&&(e.preventDefault(),t&&e.stopPropagation())},B=(e,t,i,s)=>{e.addEventListener(t,i,s)},K=(e,t)=>!!t&&(!!t[e]&&1===(t.altKey?1:0)+(t.ctrlKey?1:0)+(t.shiftKey?1:0)+(t.metaKey?1:0)),M=(e,t)=>{const i=e.getAttribute("id")
return i||(e.setAttribute("id",t),t)},Q=e=>e.replace(/[\\"']/g,"\\$&"),G=(e,t)=>{t&&e.append(t)}
function U(e,t){var i=Object.assign({},j,t),s=i.dataAttr,n=i.labelField,o=i.valueField,r=i.disabledField,l=i.optgroupField,a=i.optgroupLabelField,c=i.optgroupValueField,d=e.tagName.toLowerCase(),p=e.getAttribute("placeholder")||e.getAttribute("data-placeholder")
if(!p&&!i.allowEmptyOption){let t=e.querySelector('option[value=""]')
t&&(p=t.textContent)}var u,h,g,f,v,m,O={placeholder:p,options:[],optgroups:[],items:[],maxItems:null}
return"select"===d?(h=O.options,g={},f=1,v=e=>{var t=Object.assign({},e.dataset),i=s&&t[s]
return"string"==typeof i&&i.length&&(t=Object.assign(t,JSON.parse(i))),t},m=(e,t)=>{var s=q(e.value)
if(null!=s&&(s||i.allowEmptyOption)){if(g.hasOwnProperty(s)){if(t){var a=g[s][l]
a?Array.isArray(a)?a.push(t):g[s][l]=[a,t]:g[s][l]=t}}else{var c=v(e)
c[n]=c[n]||e.textContent,c[o]=c[o]||s,c[r]=c[r]||e.disabled,c[l]=c[l]||t,c.$option=e,g[s]=c,h.push(c)}e.selected&&O.items.push(s)}},O.maxItems=e.hasAttribute("multiple")?null:1,y(e.children,(e=>{var t,i,s
"optgroup"===(u=e.tagName.toLowerCase())?((s=v(t=e))[a]=s[a]||t.getAttribute("label")||"",s[c]=s[c]||f++,s[r]=s[r]||t.disabled,O.optgroups.push(s),i=s[c],y(t.children,(e=>{m(e,i)}))):"option"===u&&m(e)}))):(()=>{const t=e.getAttribute(s)
if(t)O.options=JSON.parse(t),y(O.options,(e=>{O.items.push(e[o])}))
else{var r=e.value.trim()||""
if(!i.allowEmptyOption&&!r.length)return
const t=r.split(i.delimiter)
y(t,(e=>{const t={}
t[n]=e,t[o]=e,O.options.push(t)})),O.items=t}})(),Object.assign({},j,O,t)}var W=0
class J extends(function(e){return e.plugins={},class extends e{constructor(...e){super(...e),this.plugins={names:[],settings:{},requested:{},loaded:{}}}static define(t,i){e.plugins[t]={name:t,fn:i}}initializePlugins(e){var t,i
const s=this,n=[]
if(Array.isArray(e))e.forEach((e=>{"string"==typeof e?n.push(e):(s.plugins.settings[e.name]=e.options,n.push(e.name))}))
else if(e)for(t in e)e.hasOwnProperty(t)&&(s.plugins.settings[t]=e[t],n.push(t))
for(;i=n.shift();)s.require(i)}loadPlugin(t){var i=this,s=i.plugins,n=e.plugins[t]
if(!e.plugins.hasOwnProperty(t))throw new Error('Unable to find "'+t+'" plugin')
s.requested[t]=!0,s.loaded[t]=n.fn.apply(i,[i.plugins.settings[t]||{}]),s.names.push(t)}require(e){var t=this,i=t.plugins
if(!t.plugins.loaded.hasOwnProperty(e)){if(i.requested[e])throw new Error('Plugin has circular dependency ("'+e+'")')
t.loadPlugin(e)}return i.loaded[e]}}}(t)){constructor(e,t){var i
super(),this.order=0,this.isOpen=!1,this.isDisabled=!1,this.isInvalid=!1,this.isValid=!0,this.isLocked=!1,this.isFocused=!1,this.isInputHidden=!1,this.isSetup=!1,this.ignoreFocus=!1,this.hasOptions=!1,this.lastValue="",this.caretPos=0,this.loading=0,this.loadedSearches={},this.activeOption=null,this.activeItems=[],this.optgroups={},this.options={},this.userOptions={},this.items=[],W++
var s=w(e)
if(s.tomselect)throw new Error("Tom Select already initialized on this element")
s.tomselect=this,i=(window.getComputedStyle&&window.getComputedStyle(s,null)).getPropertyValue("direction")
const n=U(s,t)
this.settings=n,this.input=s,this.tabIndex=s.tabIndex||0,this.is_select_tag="select"===s.tagName.toLowerCase(),this.rtl=/rtl/i.test(i),this.inputId=M(s,"tomselect-"+W),this.isRequired=s.required,this.sifter=new b(this.options,{diacritics:n.diacritics}),n.mode=n.mode||(1===n.maxItems?"single":"multi"),"boolean"!=typeof n.hideSelected&&(n.hideSelected="multi"===n.mode),"boolean"!=typeof n.hidePlaceholder&&(n.hidePlaceholder="multi"!==n.mode)
var o=n.createFilter
"function"!=typeof o&&("string"==typeof o&&(o=new RegExp(o)),o instanceof RegExp?n.createFilter=e=>o.test(e):n.createFilter=()=>!0),this.initializePlugins(n.plugins),this.setupCallbacks(),this.setupTemplates()
const r=w("<div>"),l=w("<div>"),a=this._render("dropdown"),c=w('<div role="listbox" tabindex="-1">'),d=this.input.getAttribute("class")||"",p=n.mode
var u
if(C(r,n.wrapperClass,d,p),C(l,n.controlClass),G(r,l),C(a,n.dropdownClass,p),n.copyClassesToDropdown&&C(a,d),C(c,n.dropdownContentClass),G(a,c),w(n.dropdownParent||r).appendChild(a),n.hasOwnProperty("controlInput"))n.controlInput?(u=w(n.controlInput),this.focus_node=u):(u=w("<input/>"),this.focus_node=l)
else{u=w('<input type="text" autocomplete="off" size="1" />')
y(["autocorrect","autocapitalize","autocomplete"],(e=>{s.getAttribute(e)&&P(u,{[e]:s.getAttribute(e)})})),u.tabIndex=-1,l.appendChild(u),this.focus_node=u}this.wrapper=r,this.dropdown=a,this.dropdown_content=c,this.control=l,this.control_input=u,this.setup()}setup(){const e=this,t=e.settings,i=e.control_input,s=e.dropdown,n=e.dropdown_content,o=e.wrapper,r=e.control,l=e.input,a=e.focus_node,c={passive:!0},d=e.inputId+"-ts-dropdown"
P(n,{id:d}),P(a,{role:"combobox","aria-haspopup":"listbox","aria-expanded":"false","aria-controls":d})
const p=M(a,e.inputId+"-ts-control"),u="label[for='"+(e=>e.replace(/['"\\]/g,"\\$&"))(e.inputId)+"']",h=document.querySelector(u),g=e.focus.bind(e)
if(h){B(h,"click",g),P(h,{for:p})
const t=M(h,e.inputId+"-ts-label")
P(a,{"aria-labelledby":t}),P(n,{"aria-labelledby":t})}if(o.style.width=l.style.width,e.plugins.names.length){const t="plugin-"+e.plugins.names.join(" plugin-")
C([o,s],t)}(null===t.maxItems||t.maxItems>1)&&e.is_select_tag&&P(l,{multiple:"multiple"}),e.settings.placeholder&&P(i,{placeholder:t.placeholder}),!e.settings.splitOn&&e.settings.delimiter&&(e.settings.splitOn=new RegExp("\\s*"+v(e.settings.delimiter)+"+\\s*")),t.load&&t.loadThrottle&&(t.load=z(t.load,t.loadThrottle)),e.control_input.type=l.type,B(s,"click",(t=>{const i=k(t.target,"[data-selectable]")
i&&(e.onOptionSelect(t,i),H(t,!0))})),B(r,"click",(t=>{var s=k(t.target,"[data-ts-item]",r)
s&&e.onItemSelect(t,s)?H(t,!0):""==i.value&&(e.onClick(),H(t,!0))})),B(i,"mousedown",(e=>{""!==i.value&&e.stopPropagation()})),B(a,"keydown",(t=>e.onKeyDown(t))),B(i,"keypress",(t=>e.onKeyPress(t))),B(i,"input",(t=>e.onInput(t))),B(a,"resize",(()=>e.positionDropdown()),c),B(a,"blur",(t=>e.onBlur(t))),B(a,"focus",(t=>e.onFocus(t))),B(a,"paste",(t=>e.onPaste(t)))
const f=t=>{const i=t.composedPath()[0]
if(!o.contains(i)&&!s.contains(i))return e.isFocused&&e.blur(),void e.inputState()
H(t,!0)}
var m=()=>{e.isOpen&&e.positionDropdown()}
B(document,"mousedown",f),B(window,"scroll",m,c),B(window,"resize",m,c),this._destroy=()=>{document.removeEventListener("mousedown",f),window.removeEventListener("sroll",m),window.removeEventListener("resize",m),h&&h.removeEventListener("click",g)},this.revertSettings={innerHTML:l.innerHTML,tabIndex:l.tabIndex},l.tabIndex=-1,l.insertAdjacentElement("afterend",e.wrapper),e.sync(!1),t.items=[],delete t.optgroups,delete t.options,B(l,"invalid",(t=>{e.isValid&&(e.isValid=!1,e.isInvalid=!0,e.refreshState())})),e.updateOriginalInput(),e.refreshItems(),e.close(!1),e.inputState(),e.isSetup=!0,l.disabled?e.disable():e.enable(),e.on("change",this.onChange),C(l,"tomselected","ts-hidden-accessible"),e.trigger("initialize"),!0===t.preload&&e.preload()}setupOptions(e=[],t=[]){this.addOptions(e),y(t,(e=>{this.registerOptionGroup(e)}))}setupTemplates(){var e=this,t=e.settings.labelField,i=e.settings.optgroupLabelField,s={optgroup:e=>{let t=document.createElement("div")
return t.className="optgroup",t.appendChild(e.options),t},optgroup_header:(e,t)=>'<div class="optgroup-header">'+t(e[i])+"</div>",option:(e,i)=>"<div>"+i(e[t])+"</div>",item:(e,i)=>"<div>"+i(e[t])+"</div>",option_create:(e,t)=>'<div class="create">Add <strong>'+t(e.input)+"</strong>&hellip;</div>",no_results:()=>'<div class="no-results">No results found</div>',loading:()=>'<div class="spinner"></div>',not_loading:()=>{},dropdown:()=>"<div></div>"}
e.settings.render=Object.assign({},s,e.settings.render)}setupCallbacks(){var e,t,i={initialize:"onInitialize",change:"onChange",item_add:"onItemAdd",item_remove:"onItemRemove",item_select:"onItemSelect",clear:"onClear",option_add:"onOptionAdd",option_remove:"onOptionRemove",option_clear:"onOptionClear",optgroup_add:"onOptionGroupAdd",optgroup_remove:"onOptionGroupRemove",optgroup_clear:"onOptionGroupClear",dropdown_open:"onDropdownOpen",dropdown_close:"onDropdownClose",type:"onType",load:"onLoad",focus:"onFocus",blur:"onBlur"}
for(e in i)(t=this.settings[i[e]])&&this.on(e,t)}sync(e=!0){const t=this,i=e?U(t.input,{delimiter:t.settings.delimiter}):t.settings
t.setupOptions(i.options,i.optgroups),t.setValue(i.items,!0),t.lastQuery=null}onClick(){var e=this
if(e.activeItems.length>0)return e.clearActiveItems(),void e.focus()
e.isFocused&&e.isOpen?e.blur():e.focus()}onMouseDown(){}onChange(){_(this.input,"input"),_(this.input,"change")}onPaste(e){var t=this
t.isFull()||t.isInputHidden||t.isLocked?H(e):t.settings.splitOn&&setTimeout((()=>{var e=t.inputValue()
if(e.match(t.settings.splitOn)){var i=e.trim().split(t.settings.splitOn)
y(i,(e=>{t.createItem(e)}))}}),0)}onKeyPress(e){var t=this
if(!t.isLocked){var i=String.fromCharCode(e.keyCode||e.which)
return t.settings.create&&"multi"===t.settings.mode&&i===t.settings.delimiter?(t.createItem(),void H(e)):void 0}H(e)}onKeyDown(e){var t=this
if(t.isLocked)9!==e.keyCode&&H(e)
else{switch(e.keyCode){case 65:if(K(V,e))return H(e),void t.selectAll()
break
case 27:return t.isOpen&&(H(e,!0),t.close()),void t.clearActiveItems()
case 40:if(!t.isOpen&&t.hasOptions)t.open()
else if(t.activeOption){let e=t.getAdjacent(t.activeOption,1)
e&&t.setActiveOption(e)}return void H(e)
case 38:if(t.activeOption){let e=t.getAdjacent(t.activeOption,-1)
e&&t.setActiveOption(e)}return void H(e)
case 13:return void(t.isOpen&&t.activeOption?(t.onOptionSelect(e,t.activeOption),H(e)):t.settings.create&&t.createItem()&&H(e))
case 37:return void t.advanceSelection(-1,e)
case 39:return void t.advanceSelection(1,e)
case 9:return void(t.settings.selectOnTab&&(t.isOpen&&t.activeOption&&(t.onOptionSelect(e,t.activeOption),H(e)),t.settings.create&&t.createItem()&&H(e)))
case 8:case 46:return void t.deleteSelection(e)}t.isInputHidden&&!K(V,e)&&H(e)}}onInput(e){var t=this
if(!t.isLocked){var i=t.inputValue()
t.lastValue!==i&&(t.lastValue=i,t.settings.shouldLoad.call(t,i)&&t.load(i),t.refreshOptions(),t.trigger("type",i))}}onFocus(e){var t=this,i=t.isFocused
if(t.isDisabled)return t.blur(),void H(e)
t.ignoreFocus||(t.isFocused=!0,"focus"===t.settings.preload&&t.preload(),i||t.trigger("focus"),t.activeItems.length||(t.showInput(),t.refreshOptions(!!t.settings.openOnFocus)),t.refreshState())}onBlur(e){if(!1!==document.hasFocus()){var t=this
if(t.isFocused){t.isFocused=!1,t.ignoreFocus=!1
var i=()=>{t.close(),t.setActiveItem(),t.setCaret(t.items.length),t.trigger("blur")}
t.settings.create&&t.settings.createOnBlur?t.createItem(null,!1,i):i()}}}onOptionSelect(e,t){var i,s=this
t&&(t.parentElement&&t.parentElement.matches("[data-disabled]")||(t.classList.contains("create")?s.createItem(null,!0,(()=>{s.settings.closeAfterSelect&&s.close()})):void 0!==(i=t.dataset.value)&&(s.lastQuery=null,s.addItem(i),s.settings.closeAfterSelect&&s.close(),!s.settings.hideSelected&&e.type&&/click/.test(e.type)&&s.setActiveOption(t))))}onItemSelect(e,t){var i=this
return!i.isLocked&&"multi"===i.settings.mode&&(H(e),i.setActiveItem(t,e),!0)}canLoad(e){return!!this.settings.load&&!this.loadedSearches.hasOwnProperty(e)}load(e){const t=this
if(!t.canLoad(e))return
C(t.wrapper,t.settings.loadingClass),t.loading++
const i=t.loadCallback.bind(t)
t.settings.load.call(t,e,i)}loadCallback(e,t){const i=this
i.loading=Math.max(i.loading-1,0),i.lastQuery=null,i.clearActiveOption(),i.setupOptions(e,t),i.refreshOptions(i.isFocused&&!i.isInputHidden),i.loading||S(i.wrapper,i.settings.loadingClass),i.trigger("load",e,t)}preload(){var e=this.wrapper.classList
e.contains("preloaded")||(e.add("preloaded"),this.load(""))}setTextboxValue(e=""){var t=this.control_input
t.value!==e&&(t.value=e,_(t,"update"),this.lastValue=e)}getValue(){return this.is_select_tag&&this.input.hasAttribute("multiple")?this.items:this.items.join(this.settings.delimiter)}setValue(e,t){R(this,t?[]:["change"],(()=>{this.clear(t),this.addItems(e,t)}))}setMaxItems(e){0===e&&(e=null),this.settings.maxItems=e,this.refreshState()}setActiveItem(e,t){var i,s,n,o,r,l,a=this
if("single"!==a.settings.mode){if(!e)return a.clearActiveItems(),void(a.isFocused&&a.showInput())
if("click"===(i=t&&t.type.toLowerCase())&&K("shiftKey",t)&&a.activeItems.length){for(l=a.getLastActive(),(n=Array.prototype.indexOf.call(a.control.children,l))>(o=Array.prototype.indexOf.call(a.control.children,e))&&(r=n,n=o,o=r),s=n;s<=o;s++)e=a.control.children[s],-1===a.activeItems.indexOf(e)&&a.setActiveItemClass(e)
H(t)}else"click"===i&&K(V,t)||"keydown"===i&&K("shiftKey",t)?e.classList.contains("active")?a.removeActiveItem(e):a.setActiveItemClass(e):(a.clearActiveItems(),a.setActiveItemClass(e))
a.hideInput(),a.isFocused||a.focus()}}setActiveItemClass(e){const t=this,i=t.control.querySelector(".last-active")
i&&S(i,"last-active"),C(e,"active last-active"),t.trigger("item_select",e),-1==t.activeItems.indexOf(e)&&t.activeItems.push(e)}removeActiveItem(e){var t=this.activeItems.indexOf(e)
this.activeItems.splice(t,1),S(e,"active")}clearActiveItems(){S(this.activeItems,"active"),this.activeItems=[]}setActiveOption(e){e!==this.activeOption&&(this.clearActiveOption(),e&&(this.activeOption=e,P(this.focus_node,{"aria-activedescendant":e.getAttribute("id")}),P(e,{"aria-selected":"true"}),C(e,"active"),this.scrollToOption(e)))}scrollToOption(e,t){if(!e)return
const i=this.dropdown_content,s=i.clientHeight,n=i.scrollTop||0,o=e.offsetHeight,r=e.getBoundingClientRect().top-i.getBoundingClientRect().top+n
r+o>s+n?this.scroll(r-s+o,t):r<n&&this.scroll(r,t)}scroll(e,t){const i=this.dropdown_content
t&&(i.style.scrollBehavior=t),i.scrollTop=e,i.style.scrollBehavior=""}clearActiveOption(){this.activeOption&&(S(this.activeOption,"active"),P(this.activeOption,{"aria-selected":null})),this.activeOption=null,P(this.focus_node,{"aria-activedescendant":null})}selectAll(){if("single"===this.settings.mode)return
const e=this.controlChildren()
e.length&&(this.hideInput(),this.close(),this.activeItems=e,C(e,"active"))}inputState(){var e=this
e.control.contains(e.control_input)&&(P(e.control_input,{placeholder:e.settings.placeholder}),e.activeItems.length>0||!e.isFocused&&e.settings.hidePlaceholder&&e.items.length>0?(e.setTextboxValue(),e.isInputHidden=!0):(e.settings.hidePlaceholder&&e.items.length>0&&P(e.control_input,{placeholder:""}),e.isInputHidden=!1),e.wrapper.classList.toggle("input-hidden",e.isInputHidden))}hideInput(){this.inputState()}showInput(){this.inputState()}inputValue(){return this.control_input.value.trim()}focus(){var e=this
e.isDisabled||(e.ignoreFocus=!0,e.control_input.offsetWidth?e.control_input.focus():e.focus_node.focus(),setTimeout((()=>{e.ignoreFocus=!1,e.onFocus()}),0))}blur(){this.focus_node.blur(),this.onBlur()}getScoreFunction(e){return this.sifter.getScoreFunction(e,this.getSearchOptions())}getSearchOptions(){var e=this.settings,t=e.sortField
return"string"==typeof e.sortField&&(t=[{field:e.sortField}]),{fields:e.searchField,conjunction:e.searchConjunction,sort:t,nesting:e.nesting}}search(e){var t,i,s,n=this,o=this.getSearchOptions()
if(n.settings.score&&"function"!=typeof(s=n.settings.score.call(n,e)))throw new Error('Tom Select "score" setting must be a function that returns a function')
if(e!==n.lastQuery?(n.lastQuery=e,i=n.sifter.search(e,Object.assign(o,{score:s})),n.currentResults=i):i=Object.assign({},n.currentResults),n.settings.hideSelected)for(t=i.items.length-1;t>=0;t--){let e=q(i.items[t].id)
e&&-1!==n.items.indexOf(e)&&i.items.splice(t,1)}return i}refreshOptions(e=!0){var t,i,s,n,o,r,l,a,c,d,p
const u={},h=[]
var g,f=this,v=f.inputValue(),m=f.search(v),O=f.activeOption,b=f.settings.shouldOpen||!1,w=f.dropdown_content
for(O&&(c=O.dataset.value,d=O.closest("[data-group]")),n=m.items.length,"number"==typeof f.settings.maxOptions&&(n=Math.min(n,f.settings.maxOptions)),n>0&&(b=!0),t=0;t<n;t++){let e=m.items[t].id,n=f.options[e],l=f.getOption(e,!0)
for(f.settings.hideSelected||l.classList.toggle("selected",f.items.includes(e)),o=n[f.settings.optgroupField]||"",i=0,s=(r=Array.isArray(o)?o:[o])&&r.length;i<s;i++)o=r[i],f.optgroups.hasOwnProperty(o)||(o=""),u.hasOwnProperty(o)||(u[o]=document.createDocumentFragment(),h.push(o)),i>0&&(l=l.cloneNode(!0),P(l,{id:n.$id+"-clone-"+i,"aria-selected":null}),l.classList.add("ts-cloned"),S(l,"active")),c==e&&d&&d.dataset.group===o&&(O=l),u[o].appendChild(l)}this.settings.lockOptgroupOrder&&h.sort(((e,t)=>(f.optgroups[e]&&f.optgroups[e].$order||0)-(f.optgroups[t]&&f.optgroups[t].$order||0))),l=document.createDocumentFragment(),y(h,(e=>{if(f.optgroups.hasOwnProperty(e)&&u[e].children.length){let t=document.createDocumentFragment(),i=f.render("optgroup_header",f.optgroups[e])
G(t,i),G(t,u[e])
let s=f.render("optgroup",{group:f.optgroups[e],options:t})
G(l,s)}else G(l,u[e])})),w.innerHTML="",G(w,l),f.settings.highlight&&(g=w.querySelectorAll("span.highlight"),Array.prototype.forEach.call(g,(function(e){var t=e.parentNode
t.replaceChild(e.firstChild,e),t.normalize()})),m.query.length&&m.tokens.length&&y(m.tokens,(e=>{T(w,e.regex)})))
var _=e=>{let t=f.render(e,{input:v})
return t&&(b=!0,w.insertBefore(t,w.firstChild)),t}
if(f.loading?_("loading"):f.settings.shouldLoad.call(f,v)?0===m.items.length&&_("no_results"):_("not_loading"),(a=f.canCreate(v))&&(p=_("option_create")),f.hasOptions=m.items.length>0||a,b){if(m.items.length>0){if(!w.contains(O)&&"single"===f.settings.mode&&f.items.length&&(O=f.getOption(f.items[0])),!w.contains(O)){let e=0
p&&!f.settings.addPrecedence&&(e=1),O=f.selectable()[e]}}else p&&(O=p)
e&&!f.isOpen&&(f.open(),f.scrollToOption(O,"auto")),f.setActiveOption(O)}else f.clearActiveOption(),e&&f.isOpen&&f.close(!1)}selectable(){return this.dropdown_content.querySelectorAll("[data-selectable]")}addOption(e,t=!1){const i=this
if(Array.isArray(e))return i.addOptions(e,t),!1
const s=q(e[i.settings.valueField])
return null!==s&&!i.options.hasOwnProperty(s)&&(e.$order=e.$order||++i.order,e.$id=i.inputId+"-opt-"+e.$order,i.options[s]=e,i.lastQuery=null,t&&(i.userOptions[s]=t,i.trigger("option_add",s,e)),s)}addOptions(e,t=!1){y(e,(e=>{this.addOption(e,t)}))}registerOption(e){return this.addOption(e)}registerOptionGroup(e){var t=q(e[this.settings.optgroupValueField])
return null!==t&&(e.$order=e.$order||++this.order,this.optgroups[t]=e,t)}addOptionGroup(e,t){var i
t[this.settings.optgroupValueField]=e,(i=this.registerOptionGroup(t))&&this.trigger("optgroup_add",i,t)}removeOptionGroup(e){this.optgroups.hasOwnProperty(e)&&(delete this.optgroups[e],this.clearCache(),this.trigger("optgroup_remove",e))}clearOptionGroups(){this.optgroups={},this.clearCache(),this.trigger("optgroup_clear")}updateOption(e,t){const i=this
var s,n
const o=q(e),r=q(t[i.settings.valueField])
if(null===o)return
if(!i.options.hasOwnProperty(o))return
if("string"!=typeof r)throw new Error("Value must be set in option data")
const l=i.getOption(o),a=i.getItem(o)
if(t.$order=t.$order||i.options[o].$order,delete i.options[o],i.uncacheValue(r),i.options[r]=t,l){if(i.dropdown_content.contains(l)){const e=i._render("option",t)
E(l,e),i.activeOption===l&&i.setActiveOption(e)}l.remove()}a&&(-1!==(n=i.items.indexOf(o))&&i.items.splice(n,1,r),s=i._render("item",t),a.classList.contains("active")&&C(s,"active"),E(a,s)),i.lastQuery=null}removeOption(e,t){const i=this
e=D(e),i.uncacheValue(e),delete i.userOptions[e],delete i.options[e],i.lastQuery=null,i.trigger("option_remove",e),i.removeItem(e,t)}clearOptions(){this.loadedSearches={},this.userOptions={},this.clearCache()
var e={}
y(this.options,((t,i)=>{this.items.indexOf(i)>=0&&(e[i]=this.options[i])})),this.options=this.sifter.items=e,this.lastQuery=null,this.trigger("option_clear")}getOption(e,t=!1){const i=q(e)
if(null!==i&&this.options.hasOwnProperty(i)){const e=this.options[i]
if(e.$div)return e.$div
if(t)return this._render("option",e)}return null}getAdjacent(e,t,i="option"){var s
if(!e)return null
s="item"==i?this.controlChildren():this.dropdown_content.querySelectorAll("[data-selectable]")
for(let i=0;i<s.length;i++)if(s[i]==e)return t>0?s[i+1]:s[i-1]
return null}getItem(e){if("object"==typeof e)return e
var t=q(e)
return null!==t?this.control.querySelector(`[data-value="${Q(t)}"]`):null}addItems(e,t){var i=this,s=Array.isArray(e)?e:[e]
for(let e=0,n=(s=s.filter((e=>-1===i.items.indexOf(e)))).length;e<n;e++)i.isPending=e<n-1,i.addItem(s[e],t)}addItem(e,t){R(this,t?[]:["change"],(()=>{var i,s
const n=this,o=n.settings.mode,r=q(e)
if((!r||-1===n.items.indexOf(r)||("single"===o&&n.close(),"single"!==o&&n.settings.duplicates))&&null!==r&&n.options.hasOwnProperty(r)&&("single"===o&&n.clear(t),"multi"!==o||!n.isFull())){if(i=n._render("item",n.options[r]),n.control.contains(i)&&(i=i.cloneNode(!0)),s=n.isFull(),n.items.splice(n.caretPos,0,r),n.insertAtCaret(i),n.isSetup){if(!n.isPending&&n.settings.hideSelected){let e=n.getOption(r),t=n.getAdjacent(e,1)
t&&n.setActiveOption(t)}n.isPending||n.refreshOptions(n.isFocused&&"single"!==o),0!=n.settings.closeAfterSelect&&n.isFull()?n.close():n.isPending||n.positionDropdown(),n.trigger("item_add",r,i),n.isPending||n.updateOriginalInput({silent:t})}(!n.isPending||!s&&n.isFull())&&(n.inputState(),n.refreshState())}}))}removeItem(e=null,t){const i=this
if(!(e=i.getItem(e)))return
var s,n
const o=e.dataset.value
s=L(e),e.remove(),e.classList.contains("active")&&(n=i.activeItems.indexOf(e),i.activeItems.splice(n,1),S(e,"active")),i.items.splice(s,1),i.lastQuery=null,!i.settings.persist&&i.userOptions.hasOwnProperty(o)&&i.removeOption(o,t),s<i.caretPos&&i.setCaret(i.caretPos-1),i.updateOriginalInput({silent:t}),i.refreshState(),i.positionDropdown(),i.trigger("item_remove",o,e)}createItem(e=null,t=!0,i=(()=>{})){var s,n=this,o=n.caretPos
if(e=e||n.inputValue(),!n.canCreate(e))return i(),!1
n.lock()
var r=!1,l=e=>{if(n.unlock(),!e||"object"!=typeof e)return i()
var s=q(e[n.settings.valueField])
if("string"!=typeof s)return i()
n.setTextboxValue(),n.addOption(e,!0),n.setCaret(o),n.addItem(s),n.refreshOptions(t&&"single"!==n.settings.mode),i(e),r=!0}
return s="function"==typeof n.settings.create?n.settings.create.call(this,e,l):{[n.settings.labelField]:e,[n.settings.valueField]:e},r||l(s),!0}refreshItems(){var e=this
e.lastQuery=null,e.isSetup&&e.addItems(e.items),e.updateOriginalInput(),e.refreshState()}refreshState(){const e=this
e.refreshValidityState()
const t=e.isFull(),i=e.isLocked
e.wrapper.classList.toggle("rtl",e.rtl)
const s=e.wrapper.classList
var n
s.toggle("focus",e.isFocused),s.toggle("disabled",e.isDisabled),s.toggle("required",e.isRequired),s.toggle("invalid",!e.isValid),s.toggle("locked",i),s.toggle("full",t),s.toggle("input-active",e.isFocused&&!e.isInputHidden),s.toggle("dropdown-active",e.isOpen),s.toggle("has-options",(n=e.options,0===Object.keys(n).length)),s.toggle("has-items",e.items.length>0)}refreshValidityState(){var e=this
e.input.checkValidity&&(e.isValid=e.input.checkValidity(),e.isInvalid=!e.isValid)}isFull(){return null!==this.settings.maxItems&&this.items.length>=this.settings.maxItems}updateOriginalInput(e={}){const t=this
var i,s
const n=t.input.querySelector('option[value=""]')
if(t.is_select_tag){const e=[]
function o(i,s,o){return i||(i=w('<option value="'+N(s)+'">'+N(o)+"</option>")),i!=n&&t.input.append(i),e.push(i),i.selected=!0,i}t.input.querySelectorAll("option:checked").forEach((e=>{e.selected=!1})),0==t.items.length&&"single"==t.settings.mode?o(n,"",""):t.items.forEach((n=>{if(i=t.options[n],s=i[t.settings.labelField]||"",e.includes(i.$option)){o(t.input.querySelector(`option[value="${Q(n)}"]:not(:checked)`),n,s)}else i.$option=o(i.$option,n,s)}))}else t.input.value=t.getValue()
t.isSetup&&(e.silent||t.trigger("change",t.getValue()))}open(){var e=this
e.isLocked||e.isOpen||"multi"===e.settings.mode&&e.isFull()||(e.isOpen=!0,P(e.focus_node,{"aria-expanded":"true"}),e.refreshState(),I(e.dropdown,{visibility:"hidden",display:"block"}),e.positionDropdown(),I(e.dropdown,{visibility:"visible",display:"block"}),e.focus(),e.trigger("dropdown_open",e.dropdown))}close(e=!0){var t=this,i=t.isOpen
e&&(t.setTextboxValue(),"single"===t.settings.mode&&t.items.length&&t.hideInput()),t.isOpen=!1,P(t.focus_node,{"aria-expanded":"false"}),I(t.dropdown,{display:"none"}),t.settings.hideSelected&&t.clearActiveOption(),t.refreshState(),i&&t.trigger("dropdown_close",t.dropdown)}positionDropdown(){if("body"===this.settings.dropdownParent){var e=this.control,t=e.getBoundingClientRect(),i=e.offsetHeight+t.top+window.scrollY,s=t.left+window.scrollX
I(this.dropdown,{width:t.width+"px",top:i+"px",left:s+"px"})}}clear(e){var t=this
if(t.items.length){var i=t.controlChildren()
y(i,(e=>{t.removeItem(e,!0)})),t.showInput(),e||t.updateOriginalInput(),t.trigger("clear")}}insertAtCaret(e){const t=this,i=t.caretPos,s=t.control
s.insertBefore(e,s.children[i]),t.setCaret(i+1)}deleteSelection(e){var t,i,s,n,o,r=this
t=e&&8===e.keyCode?-1:1,i={start:(o=r.control_input).selectionStart||0,length:(o.selectionEnd||0)-(o.selectionStart||0)}
const l=[]
if(r.activeItems.length)n=F(r.activeItems,t),s=L(n),t>0&&s++,y(r.activeItems,(e=>l.push(e)))
else if((r.isFocused||"single"===r.settings.mode)&&r.items.length){const e=r.controlChildren()
t<0&&0===i.start&&0===i.length?l.push(e[r.caretPos-1]):t>0&&i.start===r.inputValue().length&&l.push(e[r.caretPos])}const a=l.map((e=>e.dataset.value))
if(!a.length||"function"==typeof r.settings.onDelete&&!1===r.settings.onDelete.call(r,a,e))return!1
for(H(e,!0),void 0!==s&&r.setCaret(s);l.length;)r.removeItem(l.pop())
return r.showInput(),r.positionDropdown(),r.refreshOptions(!1),!0}advanceSelection(e,t){var i,s,n=this
n.rtl&&(e*=-1),n.inputValue().length||(K(V,t)||K("shiftKey",t)?(s=(i=n.getLastActive(e))?i.classList.contains("active")?n.getAdjacent(i,e,"item"):i:e>0?n.control_input.nextElementSibling:n.control_input.previousElementSibling)&&(s.classList.contains("active")&&n.removeActiveItem(i),n.setActiveItemClass(s)):n.moveCaret(e))}moveCaret(e){}getLastActive(e){let t=this.control.querySelector(".last-active")
if(t)return t
var i=this.control.querySelectorAll(".active")
return i?F(i,e):void 0}setCaret(e){this.caretPos=this.items.length}controlChildren(){return Array.from(this.control.querySelectorAll("[data-ts-item]"))}lock(){this.close(),this.isLocked=!0,this.refreshState()}unlock(){this.isLocked=!1,this.refreshState()}disable(){var e=this
e.input.disabled=!0,e.control_input.disabled=!0,e.focus_node.tabIndex=-1,e.isDisabled=!0,e.lock()}enable(){var e=this
e.input.disabled=!1,e.control_input.disabled=!1,e.focus_node.tabIndex=e.tabIndex,e.isDisabled=!1,e.unlock()}destroy(){var e=this,t=e.revertSettings
e.trigger("destroy"),e.off(),e.wrapper.remove(),e.dropdown.remove(),e.input.innerHTML=t.innerHTML,e.input.tabIndex=t.tabIndex,S(e.input,"tomselected","ts-hidden-accessible"),e._destroy(),delete e.input.tomselect}render(e,t){return"function"!=typeof this.settings.render[e]?null:this._render(e,t)}_render(e,t){var i,s,n=""
const o=this
return"option"!==e&&"item"!=e||(n=D(t[o.settings.valueField])),null==(s=o.settings.render[e].call(this,t,N))||(s=w(s),"option"===e||"option_create"===e?t[o.settings.disabledField]?P(s,{"aria-disabled":"true"}):P(s,{"data-selectable":""}):"optgroup"===e&&(i=t.group[o.settings.optgroupValueField],P(s,{"data-group":i}),t.group[o.settings.disabledField]&&P(s,{"data-disabled":""})),"option"!==e&&"item"!==e||(P(s,{"data-value":n}),"item"===e?(C(s,o.settings.itemClass),P(s,{"data-ts-item":""})):(C(s,o.settings.optionClass),P(s,{role:"option",id:t.$id}),o.options[n].$div=s))),s}clearCache(){y(this.options,((e,t)=>{e.$div&&(e.$div.remove(),delete e.$div)}))}uncacheValue(e){const t=this.getOption(e)
t&&t.remove()}canCreate(e){return this.settings.create&&e.length>0&&this.settings.createFilter.call(this,e)}hook(e,t,i){var s=this,n=s[t]
s[t]=function(){var t,o
return"after"===e&&(t=n.apply(s,arguments)),o=i.apply(s,arguments),"instead"===e?o:("before"===e&&(t=n.apply(s,arguments)),t)}}}return J.define("change_listener",(function(){B(this.input,"change",(()=>{this.sync()}))})),J.define("checkbox_options",(function(){var e=this,t=e.onOptionSelect
e.settings.hideSelected=!1
var i=function(e){setTimeout((()=>{var t=e.querySelector("input")
e.classList.contains("selected")?t.checked=!0:t.checked=!1}),1)}
e.hook("after","setupTemplates",(()=>{var t=e.settings.render.option
e.settings.render.option=(i,s)=>{var n=w(t.call(e,i,s)),o=document.createElement("input")
o.addEventListener("click",(function(e){H(e)})),o.type="checkbox"
const r=q(i[e.settings.valueField])
return r&&e.items.indexOf(r)>-1&&(o.checked=!0),n.prepend(o),n}})),e.on("item_remove",(t=>{var s=e.getOption(t)
s&&(s.classList.remove("selected"),i(s))})),e.hook("instead","onOptionSelect",((s,n)=>{if(n.classList.contains("selected"))return n.classList.remove("selected"),e.removeItem(n.dataset.value),e.refreshOptions(),void H(s,!0)
t.call(e,s,n),i(n)}))})),J.define("clear_button",(function(e){const t=this,i=Object.assign({className:"clear-button",title:"Clear All",html:e=>`<div class="${e.className}" title="${e.title}">&times;</div>`},e)
t.on("initialize",(()=>{var e=w(i.html(i))
e.addEventListener("click",(e=>{t.clear(),"single"===t.settings.mode&&t.settings.allowEmptyOption&&t.addItem(""),e.preventDefault(),e.stopPropagation()})),t.control.appendChild(e)}))})),J.define("drag_drop",(function(){var e=this
if(!$.fn.sortable)throw new Error('The "drag_drop" plugin requires jQuery UI "sortable".')
if("multi"===e.settings.mode){var t=e.lock,i=e.unlock
e.hook("instead","lock",(()=>{var i=$(e.control).data("sortable")
return i&&i.disable(),t.call(e)})),e.hook("instead","unlock",(()=>{var t=$(e.control).data("sortable")
return t&&t.enable(),i.call(e)})),e.on("initialize",(()=>{var t=$(e.control).sortable({items:"[data-value]",forcePlaceholderSize:!0,disabled:e.isLocked,start:(e,i)=>{i.placeholder.css("width",i.helper.css("width")),t.css({overflow:"visible"})},stop:()=>{t.css({overflow:"hidden"})
var i=[]
t.children("[data-value]").each((function(){this.dataset.value&&i.push(this.dataset.value)})),e.setValue(i)}})}))}})),J.define("dropdown_header",(function(e){const t=this,i=Object.assign({title:"Untitled",headerClass:"dropdown-header",titleRowClass:"dropdown-header-title",labelClass:"dropdown-header-label",closeClass:"dropdown-header-close",html:e=>'<div class="'+e.headerClass+'"><div class="'+e.titleRowClass+'"><span class="'+e.labelClass+'">'+e.title+'</span><a class="'+e.closeClass+'">&times;</a></div></div>'},e)
t.on("initialize",(()=>{var e=w(i.html(i)),s=e.querySelector("."+i.closeClass)
s&&s.addEventListener("click",(e=>{H(e,!0),t.close()})),t.dropdown.insertBefore(e,t.dropdown.firstChild)}))})),J.define("caret_position",(function(){var e=this
e.hook("instead","setCaret",(t=>{"single"!==e.settings.mode&&e.control.contains(e.control_input)?(t=Math.max(0,Math.min(e.items.length,t)))==e.caretPos||e.isPending||e.controlChildren().forEach(((i,s)=>{s<t?e.control_input.insertAdjacentElement("beforebegin",i):e.control.appendChild(i)})):t=e.items.length,e.caretPos=t})),e.hook("instead","moveCaret",(t=>{if(!e.isFocused)return
const i=e.getLastActive(t)
if(i){const s=L(i)
e.setCaret(t>0?s+1:s),e.setActiveItem()}else e.setCaret(e.caretPos+t)}))})),J.define("dropdown_input",(function(){var e=this
e.settings.shouldOpen=!0,e.hook("before","setup",(()=>{e.focus_node=e.control,C(e.control_input,"dropdown-input")
const t=w('<div class="dropdown-input-wrap">')
t.append(e.control_input),e.dropdown.insertBefore(t,e.dropdown.firstChild)})),e.on("initialize",(()=>{e.control_input.addEventListener("keydown",(t=>{switch(t.keyCode){case 27:return e.isOpen&&(H(t,!0),e.close()),void e.clearActiveItems()
case 9:e.focus_node.tabIndex=-1}return e.onKeyDown.call(e,t)})),e.on("blur",(()=>{e.focus_node.tabIndex=e.isDisabled?-1:e.tabIndex})),e.on("dropdown_open",(()=>{e.control_input.focus()}))
const t=e.onBlur
e.hook("instead","onBlur",(i=>{if(!i||i.relatedTarget!=e.control_input)return t.call(e)})),B(e.control_input,"blur",(()=>e.onBlur())),e.hook("before","close",(()=>{e.isOpen&&e.focus_node.focus()}))}))})),J.define("input_autogrow",(function(){var e=this
e.on("initialize",(()=>{var t=document.createElement("span"),i=e.control_input
t.style.cssText="position:absolute; top:-99999px; left:-99999px; width:auto; padding:0; white-space:pre; ",e.wrapper.appendChild(t)
for(const e of["letterSpacing","fontSize","fontFamily","fontWeight","textTransform"])t.style[e]=i.style[e]
var s=()=>{e.items.length>0?(t.textContent=i.value,i.style.width=t.clientWidth+"px"):i.style.width=""}
s(),e.on("update item_add item_remove",s),B(i,"input",s),B(i,"keyup",s),B(i,"blur",s),B(i,"update",s)}))})),J.define("no_backspace_delete",(function(){var e=this,t=e.deleteSelection
this.hook("instead","deleteSelection",(i=>!!e.activeItems.length&&t.call(e,i)))})),J.define("no_active_items",(function(){this.hook("instead","setActiveItem",(()=>{})),this.hook("instead","selectAll",(()=>{}))})),J.define("optgroup_columns",(function(){var e=this,t=e.onKeyDown
e.hook("instead","onKeyDown",(i=>{var s,n,o,r
if(!e.isOpen||37!==i.keyCode&&39!==i.keyCode)return t.call(e,i)
r=k(e.activeOption,"[data-group]"),s=L(e.activeOption,"[data-selectable]"),r&&(r=37===i.keyCode?r.previousSibling:r.nextSibling)&&(n=(o=r.querySelectorAll("[data-selectable]"))[Math.min(o.length-1,s)])&&e.setActiveOption(n)}))})),J.define("remove_button",(function(e){const t=Object.assign({label:"&times;",title:"Remove",className:"remove",append:!0},e)
var i=this
if(t.append){var s='<a href="javascript:void(0)" class="'+t.className+'" tabindex="-1" title="'+N(t.title)+'">'+t.label+"</a>"
i.hook("after","setupTemplates",(()=>{var e=i.settings.render.item
i.settings.render.item=(t,n)=>{var o=w(e.call(i,t,n)),r=w(s)
return o.appendChild(r),B(r,"mousedown",(e=>{H(e,!0)})),B(r,"click",(e=>{if(H(e,!0),!i.isLocked){var t=o.dataset.value
i.removeItem(t),i.refreshOptions(!1)}})),o}}))}})),J.define("restore_on_backspace",(function(e){const t=this,i=Object.assign({text:e=>e[t.settings.labelField]},e)
t.on("item_remove",(function(e){if(""===t.control_input.value.trim()){var s=t.options[e]
s&&t.setTextboxValue(i.text.call(t,s))}}))})),J.define("virtual_scroll",(function(){const e=this,t=e.canLoad,i=e.clearActiveOption,s=e.loadCallback
var n,o={},r=!1
if(!e.settings.firstUrl)throw"virtual_scroll plugin requires a firstUrl() method"
function l(t){return!("number"==typeof e.settings.maxOptions&&n.children.length>=e.settings.maxOptions)&&!(!(t in o)||!o[t])}e.settings.sortField=[{field:"$order"},{field:"$score"}],e.setNextUrl=function(e,t){o[e]=t},e.getUrl=function(t){if(t in o){const e=o[t]
return o[t]=!1,e}return o={},e.settings.firstUrl(t)},e.hook("instead","clearActiveOption",(()=>{if(!r)return i.call(e)})),e.hook("instead","canLoad",(i=>i in o?l(i):t.call(e,i))),e.hook("instead","loadCallback",((t,i)=>{r||e.clearOptions(),s.call(e,t,i),r=!1})),e.hook("after","refreshOptions",(()=>{const t=e.lastValue
var i
l(t)?(i=e.render("loading_more",{query:t}))&&i.setAttribute("data-selectable",""):t in o&&!n.querySelector(".no-results")&&(i=e.render("no_more_results",{query:t})),i&&(C(i,e.settings.optionClass),n.append(i))})),e.on("initialize",(()=>{n=e.dropdown_content,e.settings.render=Object.assign({},{loading_more:function(){return'<div class="loading-more-results">Loading more results ... </div>'},no_more_results:function(){return'<div class="no-more-results">No more results</div>'}},e.settings.render),n.addEventListener("scroll",(function(){n.clientHeight/(n.scrollHeight-n.scrollTop)<.95||l(e.lastValue)&&(r||(r=!0,e.load.call(e,e.lastValue)))}))}))})),J}))
var tomSelect=function(e,t){return new TomSelect(e,t)}
//# sourceMappingURL=tom-select.complete.min.js.map

View file

@ -1,334 +0,0 @@
/**
* tom-select.css (v2.0.0-rc.4)
* Copyright (c) contributors
*
* 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.
*
*/
.ts-wrapper.plugin-drag_drop.multi > .ts-control > div.ui-sortable-placeholder {
visibility: visible !important;
background: #f2f2f2 !important;
background: rgba(0, 0, 0, 0.06) !important;
border: 0 none !important;
box-shadow: inset 0 0 12px 4px #fff; }
.ts-wrapper.plugin-drag_drop .ui-sortable-placeholder::after {
content: '!';
visibility: hidden; }
.ts-wrapper.plugin-drag_drop .ui-sortable-helper {
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); }
.plugin-checkbox_options .option input {
margin-right: 0.5rem; }
.plugin-clear_button .ts-control {
padding-right: calc( 1em + (3 * 6px)) !important; }
.plugin-clear_button .clear-button {
opacity: 0;
position: absolute;
top: 8px;
right: calc(8px - 6px);
margin-right: 0 !important;
background: transparent !important;
transition: opacity 0.5s;
cursor: pointer; }
.plugin-clear_button.single .clear-button {
right: calc(8px - 6px + 2rem); }
.plugin-clear_button.focus.has-items .clear-button,
.plugin-clear_button:hover.has-items .clear-button {
opacity: 1; }
.ts-wrapper .dropdown-header {
position: relative;
padding: 10px 8px;
border-bottom: 1px solid #d0d0d0;
background: #f8f8f8;
border-radius: 3px 3px 0 0; }
.ts-wrapper .dropdown-header-close {
position: absolute;
right: 8px;
top: 50%;
color: #303030;
opacity: 0.4;
margin-top: -12px;
line-height: 20px;
font-size: 20px !important; }
.ts-wrapper .dropdown-header-close:hover {
color: black; }
.plugin-dropdown_input.focus.dropdown-active .ts-control {
box-shadow: none;
border: 1px solid #d0d0d0; }
.plugin-dropdown_input .dropdown-input {
border: 1px solid #d0d0d0;
border-width: 0 0 1px 0;
display: block;
padding: 8px 8px;
box-shadow: none;
width: 100%;
background: transparent; }
.ts-wrapper.plugin-input_autogrow.has-items .ts-control > input {
min-width: 0; }
.ts-wrapper.plugin-input_autogrow.has-items.focus .ts-control > input {
flex: none;
min-width: 4px; }
.ts-wrapper.plugin-input_autogrow.has-items.focus .ts-control > input::-webkit-input-placeholder {
color: transparent; }
.ts-wrapper.plugin-input_autogrow.has-items.focus .ts-control > input::-ms-input-placeholder {
color: transparent; }
.ts-wrapper.plugin-input_autogrow.has-items.focus .ts-control > input::placeholder {
color: transparent; }
.ts-dropdown.plugin-optgroup_columns .ts-dropdown-content {
display: flex; }
.ts-dropdown.plugin-optgroup_columns .optgroup {
border-right: 1px solid #f2f2f2;
border-top: 0 none;
flex-grow: 1;
flex-basis: 0;
min-width: 0; }
.ts-dropdown.plugin-optgroup_columns .optgroup:last-child {
border-right: 0 none; }
.ts-dropdown.plugin-optgroup_columns .optgroup:before {
display: none; }
.ts-dropdown.plugin-optgroup_columns .optgroup-header {
border-top: 0 none; }
.ts-wrapper.plugin-remove_button .item {
display: inline-flex;
align-items: center;
padding-right: 0 !important; }
.ts-wrapper.plugin-remove_button .item .remove {
color: inherit;
text-decoration: none;
vertical-align: middle;
display: inline-block;
padding: 2px 6px;
border-left: 1px solid #d0d0d0;
border-radius: 0 2px 2px 0;
box-sizing: border-box;
margin-left: 6px; }
.ts-wrapper.plugin-remove_button .item .remove:hover {
background: rgba(0, 0, 0, 0.05); }
.ts-wrapper.plugin-remove_button .item.active .remove {
border-left-color: #cacaca; }
.ts-wrapper.plugin-remove_button.disabled .item .remove:hover {
background: none; }
.ts-wrapper.plugin-remove_button.disabled .item .remove {
border-left-color: white; }
.ts-wrapper.plugin-remove_button .remove-single {
position: absolute;
right: 0;
top: 0;
font-size: 23px; }
.ts-wrapper {
position: relative; }
.ts-dropdown,
.ts-control,
.ts-control input {
color: #303030;
font-family: inherit;
font-size: 13px;
line-height: 18px;
font-smoothing: inherit; }
.ts-control,
.ts-wrapper.single.input-active .ts-control {
background: #fff;
cursor: text; }
.ts-control {
border: 1px solid #d0d0d0;
padding: 8px 8px;
width: 100%;
overflow: hidden;
position: relative;
z-index: 1;
box-sizing: border-box;
box-shadow: none;
border-radius: 3px;
display: flex;
flex-wrap: wrap; }
.ts-wrapper.multi.has-items .ts-control {
padding: calc( 8px - 2px - 0) 8px calc( 8px - 2px - 3px - 0); }
.full .ts-control {
background-color: #fff; }
.disabled .ts-control,
.disabled .ts-control * {
cursor: default !important; }
.focus .ts-control {
box-shadow: none; }
.ts-control > * {
vertical-align: baseline;
display: inline-block; }
.ts-wrapper.multi .ts-control > div {
cursor: pointer;
margin: 0 3px 3px 0;
padding: 2px 6px;
background: #f2f2f2;
color: #303030;
border: 0 solid #d0d0d0; }
.ts-wrapper.multi .ts-control > div.active {
background: #e8e8e8;
color: #303030;
border: 0 solid #cacaca; }
.ts-wrapper.multi.disabled .ts-control > div, .ts-wrapper.multi.disabled .ts-control > div.active {
color: #7d7c7c;
background: white;
border: 0 solid white; }
.ts-control > input {
flex: 1 1 auto;
min-width: 7rem;
display: inline-block !important;
padding: 0 !important;
min-height: 0 !important;
max-height: none !important;
max-width: 100% !important;
margin: 0 !important;
text-indent: 0 !important;
border: 0 none !important;
background: none !important;
line-height: inherit !important;
-webkit-user-select: auto !important;
-moz-user-select: auto !important;
-ms-user-select: auto !important;
user-select: auto !important;
box-shadow: none !important; }
.ts-control > input::-ms-clear {
display: none; }
.ts-control > input:focus {
outline: none !important; }
.has-items .ts-control > input {
margin: 0 4px !important; }
.ts-control.rtl {
text-align: right; }
.ts-control.rtl.single .ts-control:after {
left: 15px;
right: auto; }
.ts-control.rtl .ts-control > input {
margin: 0 4px 0 -2px !important; }
.disabled .ts-control {
opacity: 0.5;
background-color: #fafafa; }
.input-hidden .ts-control > input {
opacity: 0;
position: absolute;
left: -10000px; }
.ts-dropdown {
position: absolute;
top: 100%;
left: 0;
width: 100%;
z-index: 10;
border: 1px solid #d0d0d0;
background: #fff;
margin: 0.25rem 0 0 0;
border-top: 0 none;
box-sizing: border-box;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
border-radius: 0 0 3px 3px; }
.ts-dropdown [data-selectable] {
cursor: pointer;
overflow: hidden; }
.ts-dropdown [data-selectable] .highlight {
background: rgba(125, 168, 208, 0.2);
border-radius: 1px; }
.ts-dropdown .option,
.ts-dropdown .optgroup-header,
.ts-dropdown .no-results,
.ts-dropdown .create {
padding: 5px 8px; }
.ts-dropdown .option, .ts-dropdown [data-disabled], .ts-dropdown [data-disabled] [data-selectable].option {
cursor: inherit;
opacity: 0.5; }
.ts-dropdown [data-selectable].option {
opacity: 1;
cursor: pointer; }
.ts-dropdown .optgroup:first-child .optgroup-header {
border-top: 0 none; }
.ts-dropdown .optgroup-header {
color: #303030;
background: #fff;
cursor: default; }
.ts-dropdown .create:hover,
.ts-dropdown .option:hover,
.ts-dropdown .active {
background-color: #f5fafd;
color: #495c68; }
.ts-dropdown .create:hover.create,
.ts-dropdown .option:hover.create,
.ts-dropdown .active.create {
color: #495c68; }
.ts-dropdown .create {
color: rgba(48, 48, 48, 0.5); }
.ts-dropdown .spinner {
display: inline-block;
width: 30px;
height: 30px;
margin: 5px 8px; }
.ts-dropdown .spinner:after {
content: " ";
display: block;
width: 24px;
height: 24px;
margin: 3px;
border-radius: 50%;
border: 5px solid #d0d0d0;
border-color: #d0d0d0 transparent #d0d0d0 transparent;
animation: lds-dual-ring 1.2s linear infinite; }
@keyframes lds-dual-ring {
0% {
transform: rotate(0deg); }
100% {
transform: rotate(360deg); } }
.ts-dropdown-content {
overflow-y: auto;
overflow-x: hidden;
max-height: 200px;
overflow-scrolling: touch;
scroll-behavior: smooth; }
.ts-hidden-accessible {
border: 0 !important;
clip: rect(0 0 0 0) !important;
-webkit-clip-path: inset(50%) !important;
clip-path: inset(50%) !important;
height: 1px !important;
overflow: hidden !important;
padding: 0 !important;
position: absolute !important;
width: 1px !important;
white-space: nowrap !important; }
/*# sourceMappingURL=tom-select.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

286
web/server.py Normal file
View file

@ -0,0 +1,286 @@
"""
FastAPI server for memory graph visualization and API.
Provides REST API endpoints for memory operations and serves
the interactive visualization interface.
"""
import asyncpg
import asyncio
from fastapi import FastAPI, HTTPException
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from pydantic import BaseModel
from dotenv import load_dotenv
import os
import sys
from pathlib import Path
from typing import Optional
# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent))
from memory import TemporalSemanticMemory
load_dotenv()
app = FastAPI(title="Memory Graph API", version="1.0.0")
# Mount static files
app.mount("/static", StaticFiles(directory="web/static"), name="static")
class SearchRequest(BaseModel):
"""Request model for search endpoint."""
query: str
agent_id: str = "default"
thinking_budget: int = 100
top_k: int = 10
async def get_graph_data():
"""Fetch graph data from database."""
conn = await asyncpg.connect(
os.getenv('DATABASE_URL'),
statement_cache_size=0 # Disable statement caching for pgbouncer compatibility
)
# Get all memory units
units = await conn.fetch("""
SELECT id, text, event_date, context
FROM memory_units
ORDER BY event_date
""")
# Get all links with weights
links = await conn.fetch("""
SELECT
ml.from_unit_id,
ml.to_unit_id,
ml.link_type,
ml.weight,
e.canonical_name as entity_name
FROM memory_links ml
LEFT JOIN entities e ON ml.entity_id = e.id
ORDER BY ml.link_type, ml.weight DESC
""")
# Get entity information
unit_entities = await conn.fetch("""
SELECT ue.unit_id, e.canonical_name, e.entity_type
FROM unit_entities ue
JOIN entities e ON ue.entity_id = e.id
ORDER BY ue.unit_id
""")
await conn.close()
# Build entity mapping
entity_map = {}
for row in unit_entities:
unit_id = row['unit_id']
entity_name = row['canonical_name']
entity_type = row['entity_type']
if unit_id not in entity_map:
entity_map[unit_id] = []
entity_map[unit_id].append(f"{entity_name} ({entity_type})")
# Build nodes
nodes = []
for row in units:
unit_id = row['id']
text = row['text']
event_date = row['event_date']
context = row['context']
entities = entity_map.get(unit_id, [])
entity_count = len(entities)
# Color by entity count
if entity_count == 0:
color = "#e0e0e0"
elif entity_count == 1:
color = "#90caf9"
else:
color = "#42a5f5"
nodes.append({
"data": {
"id": str(unit_id),
"label": text[:50] + "..." if len(text) > 50 else text,
"text": text,
"context": context,
"date": str(event_date.date()),
"entities": ", ".join(entities) if entities else "None",
"color": color
}
})
# Build edges
edges = []
for row in links:
from_id = row['from_unit_id']
to_id = row['to_unit_id']
link_type = row['link_type']
weight = row['weight']
entity_name = row['entity_name']
# Set color based on link type
if link_type == 'temporal':
color = "#00bcd4"
line_style = "dashed"
elif link_type == 'semantic':
color = "#ff69b4"
line_style = "solid"
elif link_type == 'entity':
color = "#ffd700"
line_style = "solid"
else:
color = "#999999"
line_style = "solid"
edges.append({
"data": {
"id": f"{from_id}-{to_id}-{link_type}",
"source": str(from_id),
"target": str(to_id),
"weight": weight,
"linkType": link_type,
"entityName": entity_name or "",
"color": color,
"lineStyle": line_style
}
})
# Build table rows
table_rows = []
for row in units:
unit_id = row['id']
text = row['text']
event_date = row['event_date']
context = row['context']
entities = entity_map.get(unit_id, [])
entity_str = ", ".join(entities) if entities else "None"
table_rows.append({
"id": str(unit_id)[:8] + "...",
"text": text,
"context": context,
"date": str(event_date.date()),
"entities": entity_str
})
return {
"nodes": nodes,
"edges": edges,
"table_rows": table_rows,
"total_units": len(units)
}
@app.get("/")
async def index():
"""Serve the visualization page."""
return FileResponse("web/templates/index.html")
@app.get("/api/graph")
async def api_graph():
"""Get graph data from database."""
try:
data = await get_graph_data()
return data
except Exception as e:
import traceback
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
print(f"Error in /api/graph: {error_detail}")
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/search")
async def api_search(request: SearchRequest):
"""Run a search and return results with trace."""
try:
# Initialize memory system
memory = TemporalSemanticMemory()
# Run search with tracing
results, trace = await memory.search_async(
agent_id=request.agent_id,
query=request.query,
thinking_budget=request.thinking_budget,
top_k=request.top_k,
enable_trace=True
)
# Convert trace to dict
trace_dict = trace.to_dict() if trace else None
return {
'results': results,
'trace': trace_dict
}
except Exception as e:
import traceback
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
print(f"Error in /api/search: {error_detail}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/agents")
async def api_agents():
"""Get list of available agents from database."""
try:
conn = await asyncpg.connect(
os.getenv('DATABASE_URL'),
statement_cache_size=0
)
# Get distinct agent IDs from memory_units
agents = await conn.fetch("""
SELECT DISTINCT agent_id
FROM memory_units
WHERE agent_id IS NOT NULL
ORDER BY agent_id
""")
await conn.close()
agent_list = [row['agent_id'] for row in agents]
return {"agents": agent_list}
except Exception as e:
import traceback
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
print(f"Error in /api/agents: {error_detail}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/locomo")
async def api_locomo():
"""Get Locomo benchmark results."""
import json
try:
results_path = Path(__file__).parent.parent / "benchmarks" / "locomo" / "benchmark_results.json"
if not results_path.exists():
raise HTTPException(status_code=404, detail="Benchmark results not found")
with open(results_path, 'r') as f:
data = json.load(f)
return data
except FileNotFoundError:
raise HTTPException(status_code=404, detail="Benchmark results not found")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
if __name__ == "__main__":
import uvicorn
print("\n" + "=" * 80)
print("Memory Graph API Server")
print("=" * 80)
print("\nStarting server at http://localhost:8080")
print("\nEndpoints:")
print(" GET / - Visualization UI")
print(" GET /api/graph - Get graph data")
print(" POST /api/search - Run search with trace")
print("\n" + "=" * 80 + "\n")
uvicorn.run("server:app", host="0.0.0.0", port=8080, reload=True)

498
web/static/css/styles.css Normal file
View file

@ -0,0 +1,498 @@
body {
font-family: Tahoma, sans-serif;
margin: 0;
padding: 0;
background: #f5f5f5;
}
.tab-container {
background: white;
}
.tab-buttons {
background: #f0f0f0;
border-bottom: 2px solid #333;
padding: 0;
margin: 0;
}
.tab-button {
background: #e0e0e0;
border: none;
padding: 12px 24px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
border-top: 2px solid transparent;
border-left: 2px solid transparent;
border-right: 2px solid transparent;
transition: background 0.2s;
}
.tab-button:hover {
background: #d0d0d0;
}
.tab-button.active {
background: white;
border-top: 2px solid #333;
border-left: 2px solid #333;
border-right: 2px solid #333;
border-bottom: 2px solid white;
margin-bottom: -2px;
}
.tab-content {
display: none;
background: white;
}
.tab-content.active {
display: block;
}
#cy {
width: 100%;
height: 800px;
background: #ffffff;
}
#graph-tab {
position: relative;
}
#table-tab {
padding: 20px;
}
.legend {
position: absolute;
top: 80px;
left: 20px;
background: white;
padding: 15px;
border: 2px solid #333;
border-radius: 8px;
box-shadow: 2px 2px 8px rgba(0,0,0,0.3);
z-index: 1000;
max-width: 250px;
}
.legend h3 {
margin-top: 0;
border-bottom: 2px solid #333;
padding-bottom: 5px;
}
.legend-item {
margin: 8px 0;
display: flex;
align-items: center;
}
.legend-line {
width: 30px;
height: 2px;
margin-right: 10px;
}
.legend-node {
width: 20px;
height: 20px;
margin-right: 10px;
border: 1px solid #999;
border-radius: 3px;
}
#table-filter {
width: 100%;
max-width: 600px;
padding: 10px;
margin-bottom: 15px;
border: 2px solid #ccc;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
}
#memory-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
max-width: 1400px;
}
#memory-table th {
padding: 10px;
text-align: left;
border: 1px solid #ddd;
background: #f0f0f0;
}
#memory-table td {
padding: 8px;
border: 1px solid #ddd;
}
.tooltip {
position: absolute;
background: white;
border: 2px solid #333;
border-radius: 4px;
padding: 10px;
box-shadow: 2px 2px 8px rgba(0,0,0,0.3);
max-width: 300px;
font-size: 12px;
pointer-events: none;
z-index: 9999;
}
#debug-tab {
padding: 20px;
}
.debug-container {
display: flex;
gap: 20px;
height: calc(100vh - 150px);
}
.debug-pane {
flex: 1;
display: flex;
flex-direction: column;
border: 2px solid #333;
border-radius: 8px;
overflow: hidden;
}
.debug-pane-header {
background: #f0f0f0;
padding: 10px;
border-bottom: 2px solid #333;
font-weight: bold;
}
.debug-search-controls {
padding: 10px;
background: #e3f2fd;
border-bottom: 2px solid #333;
}
.debug-status-bar {
padding: 8px 15px;
background: #fff8e1;
border-bottom: 2px solid #333;
font-size: 13px;
display: flex;
align-items: center;
gap: 5px;
flex-wrap: wrap;
}
.debug-controls {
padding: 10px;
background: #f9f9f9;
border-bottom: 2px solid #333;
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.debug-viz {
flex: 1;
position: relative;
background: white;
min-height: 400px;
width: 100%;
}
.debug-viz canvas {
width: 100% !important;
height: 100% !important;
}
.debug-info {
padding: 10px;
background: #f9f9f9;
border-top: 2px solid #333;
font-size: 12px;
max-height: 200px;
overflow-y: auto;
}
.debug-button {
padding: 8px 16px;
background: #42a5f5;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
}
.debug-button:hover {
background: #1e88e5;
}
.debug-button.secondary {
background: #66bb6a;
}
.debug-button.secondary:hover {
background: #43a047;
}
.error-message {
color: #d32f2f;
padding: 10px;
background: #ffebee;
border: 1px solid #ef5350;
border-radius: 4px;
margin-top: 10px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 10px;
margin-top: 10px;
}
.stat-item {
padding: 8px;
background: white;
border: 1px solid #ddd;
border-radius: 4px;
}
.stat-label {
font-weight: bold;
color: #666;
font-size: 11px;
}
.stat-value {
font-size: 18px;
color: #333;
}
.loading {
text-align: center;
padding: 20px;
color: #666;
}
.refresh-button {
padding: 6px 15px;
background: #66bb6a;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
margin-left: 10px;
}
.refresh-button:hover {
background: #43a047;
}
/* Decision Log Styles */
.debug-viz-container {
flex: 1;
position: relative;
overflow: hidden;
}
.decision-log {
height: 100%;
overflow-y: auto;
padding: 20px;
background: #fafafa;
}
.log-header {
background: white;
border: 2px solid #333;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.log-header h3 {
margin: 0 0 10px 0;
color: #333;
}
.log-header p {
margin: 5px 0;
}
.log-explanation {
color: #666;
font-size: 13px;
line-height: 1.5;
}
.log-step {
margin-bottom: 20px;
}
.log-step-header {
background: #333;
color: white;
padding: 10px 15px;
font-weight: bold;
border-radius: 6px 6px 0 0;
font-size: 14px;
}
.log-step-explanation {
background: #e3f2fd;
border: 2px solid #333;
border-top: none;
padding: 12px 15px;
font-size: 13px;
line-height: 1.5;
}
.log-card {
background: white;
border: 2px solid #ddd;
border-radius: 6px;
padding: 15px;
margin: 10px 0;
transition: box-shadow 0.2s;
}
.log-card:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.log-card-entry {
border-color: #66bb6a;
background: #f1f8f4;
}
.log-card-result {
border-color: #ffd54f;
background: #fffef0;
}
.log-card-pruned {
border-color: #ef5350;
background: #ffebee;
}
.log-card-header {
display: flex;
gap: 10px;
margin-bottom: 10px;
flex-wrap: wrap;
}
.log-badge {
display: inline-block;
padding: 4px 10px;
border-radius: 12px;
font-size: 11px;
font-weight: bold;
text-transform: uppercase;
}
.log-badge-entry {
background: #66bb6a;
color: white;
}
.log-badge-result {
background: #ffd54f;
color: #333;
}
.log-badge-temporal {
background: #00bcd4;
color: white;
}
.log-badge-semantic {
background: #ff69b4;
color: white;
}
.log-badge-entity {
background: #ffd700;
color: #333;
}
.log-badge-pruned {
background: #ef5350;
color: white;
}
.log-memory-text {
font-size: 14px;
color: #333;
margin: 10px 0;
padding: 10px;
background: #f9f9f9;
border-left: 4px solid #42a5f5;
font-style: italic;
}
.log-details {
margin-top: 10px;
font-size: 12px;
}
.log-detail-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 0;
border-bottom: 1px solid #eee;
}
.log-detail-row:last-child {
border-bottom: none;
}
.log-detail-label {
font-weight: bold;
color: #666;
}
.log-detail-value {
color: #333;
}
.log-detail-help {
cursor: help;
margin-left: 5px;
color: #999;
font-size: 14px;
}
/* Results Table Styles */
.results-table-container {
height: 100%;
overflow-y: auto;
background: white;
}
/* Search Graph Legend */
.search-graph-legend {
position: absolute;
top: 10px;
right: 10px;
background: white;
padding: 15px;
border: 2px solid #333;
border-radius: 8px;
box-shadow: 2px 2px 8px rgba(0,0,0,0.3);
z-index: 1000;
max-width: 320px;
font-size: 12px;
}

1041
web/static/js/app.js Normal file

File diff suppressed because it is too large Load diff

208
web/static/js/locomo.js Normal file
View file

@ -0,0 +1,208 @@
// Locomo benchmark tab functionality
let locomoData = null;
window.loadLocomoResults = async function() {
try {
const response = await fetch('/api/locomo');
locomoData = await response.json();
renderLocomoResults();
} catch (e) {
document.getElementById('locomo-content').innerHTML = `
<div class="error-message">Error loading benchmark results: ${e.message}</div>
`;
}
}
function renderLocomoResults() {
if (!locomoData) return;
const content = document.getElementById('locomo-content');
// Overall stats
const overallHtml = `
<div style="background: #f9f9f9; padding: 20px; border: 2px solid #333; border-radius: 8px; margin-bottom: 20px;">
<h3 style="margin-top: 0;">Overall Performance</h3>
<div class="stats-grid">
<div class="stat-item">
<div class="stat-label">Overall Accuracy</div>
<div class="stat-value">${locomoData.overall_accuracy.toFixed(2)}%</div>
</div>
<div class="stat-item">
<div class="stat-label">Correct Answers</div>
<div class="stat-value">${locomoData.total_correct} / ${locomoData.total_questions}</div>
</div>
<div class="stat-item">
<div class="stat-label">Conversations</div>
<div class="stat-value">${locomoData.conversation_results.length}</div>
</div>
</div>
</div>
`;
// Filter controls
const filterHtml = `
<div style="margin-bottom: 20px; display: flex; gap: 10px; align-items: center;">
<label style="font-weight: bold;">Show:</label>
<label><input type="radio" name="answer-filter" value="all" checked onchange="filterAnswers()"> All Answers</label>
<label><input type="radio" name="answer-filter" value="incorrect" onchange="filterAnswers()"> Incorrect Only</label>
<label><input type="radio" name="answer-filter" value="correct" onchange="filterAnswers()"> Correct Only</label>
</div>
`;
// Build conversation sections
let conversationsHtml = '';
locomoData.conversation_results.forEach((conv, idx) => {
const accuracy = conv.metrics.accuracy.toFixed(2);
const correctCount = conv.metrics.correct;
const totalCount = conv.metrics.total;
conversationsHtml += `
<div style="margin-bottom: 30px; border: 2px solid #333; border-radius: 8px; overflow: hidden;">
<div style="background: #f0f0f0; padding: 15px; border-bottom: 2px solid #333; cursor: pointer;" onclick="toggleConversation(${idx})">
<h3 style="margin: 0; display: flex; justify-content: space-between; align-items: center;">
<span>📊 ${conv.sample_id}</span>
<span style="font-size: 18px; color: ${accuracy >= 70 ? '#43a047' : accuracy >= 50 ? '#ff9800' : '#e53935'};">
${accuracy}% (${correctCount}/${totalCount})
</span>
</h3>
</div>
<div id="conv-${idx}" style="display: none; padding: 20px;">
${renderConversationDetails(conv)}
</div>
</div>
`;
});
content.innerHTML = overallHtml + filterHtml + conversationsHtml;
}
function renderConversationDetails(conv) {
const results = conv.metrics.detailed_results;
let html = '<div class="qa-results">';
results.forEach((result, idx) => {
const isCorrect = result.is_correct;
const bgColor = isCorrect ? '#e8f5e9' : '#ffebee';
const icon = isCorrect ? '✅' : '❌';
const category = getCategoryName(result.category);
html += `
<div class="qa-item" data-correct="${isCorrect}" style="background: ${bgColor}; padding: 15px; margin-bottom: 15px; border: 1px solid #ddd; border-radius: 8px;">
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 10px;">
<div style="flex: 1;">
<div style="font-weight: bold; font-size: 16px; margin-bottom: 8px;">
${icon} Question ${idx + 1} <span style="font-size: 12px; background: #666; color: white; padding: 2px 8px; border-radius: 4px; margin-left: 8px;">${category}</span>
</div>
<div style="margin-bottom: 8px;">
<b>Q:</b> ${result.question}
</div>
</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 10px;">
<div>
<div style="font-weight: bold; color: #43a047; margin-bottom: 4px;"> Correct Answer:</div>
<div style="background: white; padding: 8px; border-radius: 4px; border: 1px solid #ccc;">
${result.correct_answer}
</div>
</div>
<div>
<div style="font-weight: bold; color: ${isCorrect ? '#43a047' : '#e53935'}; margin-bottom: 4px;">
${isCorrect ? '✓' : '✗'} Predicted Answer:
</div>
<div style="background: white; padding: 8px; border-radius: 4px; border: 1px solid #ccc;">
${result.predicted_answer}
</div>
</div>
</div>
<details style="margin-top: 10px;">
<summary style="cursor: pointer; font-weight: bold; padding: 5px; background: rgba(255,255,255,0.5); border-radius: 4px;">
📝 Show Reasoning & Retrieved Memories
</summary>
<div style="margin-top: 10px; padding: 10px; background: white; border-radius: 4px;">
<div style="margin-bottom: 10px;">
<b>System Reasoning:</b>
<div style="padding: 8px; background: #f5f5f5; border-radius: 4px; margin-top: 4px;">
${result.reasoning}
</div>
</div>
<div style="margin-bottom: 10px;">
<b>Judge Reasoning:</b>
<div style="padding: 8px; background: #f5f5f5; border-radius: 4px; margin-top: 4px;">
${result.correctness_reasoning || 'N/A'}
</div>
</div>
<div>
<b>Retrieved Memories (${result.retrieved_memories ? result.retrieved_memories.length : 0}):</b>
${renderRetrievedMemories(result.retrieved_memories)}
</div>
</div>
</details>
</div>
`;
});
html += '</div>';
return html;
}
function renderRetrievedMemories(memories) {
if (!memories || memories.length === 0) {
return '<div style="padding: 8px; color: #999;">No memories retrieved</div>';
}
let html = '<div style="margin-top: 8px;">';
memories.forEach((mem, idx) => {
html += `
<div style="padding: 8px; background: #f5f5f5; border-left: 3px solid #42a5f5; margin-bottom: 8px;">
<div style="font-size: 11px; color: #666; margin-bottom: 4px;">
Rank #${idx + 1} | Score: ${mem.score ? mem.score.toFixed(4) : 'N/A'}
</div>
<div style="font-size: 13px;">${mem.text}</div>
</div>
`;
});
html += '</div>';
return html;
}
function getCategoryName(category) {
const categories = {
1: 'Multi-hop',
2: 'Single-hop',
3: 'Temporal',
4: 'Open-domain'
};
return categories[category] || 'Unknown';
}
function toggleConversation(idx) {
const elem = document.getElementById(`conv-${idx}`);
if (elem.style.display === 'none') {
elem.style.display = 'block';
} else {
elem.style.display = 'none';
}
}
function filterAnswers() {
const filter = document.querySelector('input[name="answer-filter"]:checked').value;
const items = document.querySelectorAll('.qa-item');
items.forEach(item => {
const isCorrect = item.dataset.correct === 'true';
if (filter === 'all') {
item.style.display = 'block';
} else if (filter === 'correct' && isCorrect) {
item.style.display = 'block';
} else if (filter === 'incorrect' && !isCorrect) {
item.style.display = 'block';
} else {
item.style.display = 'none';
}
});
}

135
web/templates/index.html Normal file
View file

@ -0,0 +1,135 @@
<!DOCTYPE html>
<html>
<head>
<title>Memory Graph - Live Visualization</title>
<meta charset="utf-8">
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.28.1/cytoscape.min.js"></script>
<link rel="stylesheet" href="/static/css/styles.css">
</head>
<body>
<div class="tab-container">
<div class="tab-buttons">
<button class="tab-button active" onclick="switchTab('graph')">Graph View</button>
<button class="tab-button" onclick="switchTab('table')">Table View</button>
<button class="tab-button" onclick="switchTab('debug')">Search Debug</button>
<button class="tab-button" onclick="switchTab('locomo')">Locomo Benchmark</button>
</div>
<div id="graph-tab" class="tab-content active">
<div style="padding: 15px; background: #f9f9f9; border-bottom: 2px solid #333;">
<div style="display: flex; gap: 15px; align-items: center; flex-wrap: wrap;">
<button onclick="loadGraphData()" style="padding: 8px 20px; background: #66bb6a; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 14px;">
📊 Load Graph Data
</button>
<div>
<label style="font-weight: bold; margin-right: 5px;">Limit nodes:</label>
<input type="number" id="node-limit" value="50" min="10" max="1000" step="10"
style="width: 80px; padding: 5px; border: 1px solid #ccc; border-radius: 4px;">
</div>
<div>
<label style="font-weight: bold; margin-right: 5px;">Layout:</label>
<select id="layout-select" style="padding: 5px; border: 1px solid #ccc; border-radius: 4px;">
<option value="circle">Circle (fast)</option>
<option value="grid">Grid (fast)</option>
<option value="cose">Force-directed (slow)</option>
</select>
</div>
<button onclick="reloadGraph()" style="padding: 6px 15px; background: #42a5f5; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;">
Apply
</button>
<button onclick="loadGraphData()" class="refresh-button">
🔄 Refresh
</button>
<span id="node-count" style="color: #666; font-size: 14px;"></span>
</div>
</div>
<div id="cy"><div style="padding: 40px; text-align: center; color: #666;">
<p>Click "Load Graph Data" to visualize the memory graph</p>
</div></div>
<div class="legend">
<h3>Legend</h3>
<h4 style="margin: 10px 0 5px 0;">Link Types:</h4>
<div class="legend-item">
<div class="legend-line" style="background: #00bcd4; border-top: 1px dashed #00bcd4;"></div>
<span><b>Temporal</b></span>
</div>
<div class="legend-item">
<div class="legend-line" style="background: #ff69b4;"></div>
<span><b>Semantic</b></span>
</div>
<div class="legend-item">
<div class="legend-line" style="background: #ffd700;"></div>
<span><b>Entity</b></span>
</div>
<h4 style="margin: 15px 0 5px 0;">Nodes:</h4>
<div class="legend-item">
<div class="legend-node" style="background: #e0e0e0;"></div>
<span>No entities</span>
</div>
<div class="legend-item">
<div class="legend-node" style="background: #90caf9;"></div>
<span>1 entity</span>
</div>
<div class="legend-item">
<div class="legend-node" style="background: #42a5f5;"></div>
<span>2+ entities</span>
</div>
</div>
</div>
<div id="table-tab" class="tab-content">
<h2>Memory Units <span id="table-count"></span></h2>
<div style="margin-bottom: 15px;">
<button onclick="loadGraphData()" style="padding: 8px 20px; background: #66bb6a; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 14px;">
📊 Load Table Data
</button>
</div>
<input type="text" id="table-filter" placeholder="Filter by text, context, or entities...">
<div style="overflow-x: auto;">
<table id="memory-table">
<thead>
<tr>
<th>ID</th>
<th>Text</th>
<th>Context</th>
<th>Date</th>
<th>Entities</th>
</tr>
</thead>
<tbody id="table-body">
<tr><td colspan="5" style="padding: 40px; text-align: center; color: #666;">Click "Load Table Data" to view memory units</td></tr>
</tbody>
</table>
</div>
</div>
<div id="debug-tab" class="tab-content">
<h2>Search Debug</h2>
<div style="margin-bottom: 15px;">
<button class="debug-button secondary" onclick="addDebugPane()">+ Add Search Pane</button>
</div>
<div id="debug-panes-container" class="debug-container">
<!-- Debug panes will be added here dynamically -->
</div>
</div>
<div id="locomo-tab" class="tab-content">
<h2>Locomo Benchmark Results</h2>
<p style="color: #666; margin-bottom: 15px;">
Analyze benchmark results and debug incorrect answers.
</p>
<div style="margin-bottom: 15px;">
<button onclick="loadLocomoResults()" style="padding: 8px 20px; background: #66bb6a; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 14px;">
📊 Load Benchmark Results
</button>
</div>
<div id="locomo-content">
<p style="padding: 20px; text-align: center; color: #666;">Click "Load Benchmark Results" to view the data</p>
</div>
</div>
</div>
<script src="/static/js/app.js"></script>
<script src="/static/js/locomo.js"></script>
</body>
</html>