improvements benchmark and perf
This commit is contained in:
parent
8c698d6dfb
commit
75f40fcc3e
40 changed files with 15420 additions and 8395 deletions
100
README.md
100
README.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
1
benchmarks/common/__init__.py
Normal file
1
benchmarks/common/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
"""Common benchmark framework."""
|
||||
538
benchmarks/common/benchmark_runner.py
Normal file
538
benchmarks/common/benchmark_runner.py
Normal 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
258
benchmarks/locomo/locomo_benchmark.py
Normal file
258
benchmarks/locomo/locomo_benchmark.py
Normal 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)}"
|
||||
8
benchmarks/locomo/results_table.md
Normal file
8
benchmarks/locomo/results_table.md
Normal 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 |
|
||||
|
|
@ -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")
|
||||
))
|
||||
|
|
|
|||
254
benchmarks/longmemeval/longmemeval_benchmark.py
Normal file
254
benchmarks/longmemeval/longmemeval_benchmark.py
Normal 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)}"
|
||||
|
|
@ -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
180
examples/trace_example.py
Normal 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())
|
||||
|
|
@ -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)
|
||||
}
|
||||
356
lib/tom-select/tom-select.complete.min.js
vendored
356
lib/tom-select/tom-select.complete.min.js
vendored
|
|
@ -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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""),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>…</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}">×</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+'">×</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:"×",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
|
||||
|
|
@ -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
27
lib/vis-9.1.2/vis-network.min.js
vendored
27
lib/vis-9.1.2/vis-network.min.js
vendored
File diff suppressed because one or more lines are too long
|
|
@ -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
166
memory/search_trace.py
Normal 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
322
memory/search_tracer.py
Normal 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
|
||||
|
|
@ -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]:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
|
|
|
|||
8
migrations/001_add_links_composite_index.sql
Normal file
8
migrations/001_add_links_composite_index.sql
Normal 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;
|
||||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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
3
serve.sh
Executable 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
185
tests/test_search_trace.py
Normal 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
505
uv.lock
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""),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>…</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}">×</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+'">×</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:"×",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
|
||||
|
|
@ -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
27
visualizations/lib/vis-9.1.2/vis-network.min.js
vendored
27
visualizations/lib/vis-9.1.2/vis-network.min.js
vendored
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
286
web/server.py
Normal 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
498
web/static/css/styles.css
Normal 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
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
208
web/static/js/locomo.js
Normal 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
135
web/templates/index.html
Normal 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>
|
||||
Loading…
Reference in a new issue