lot of improvements
This commit is contained in:
parent
be8570df5d
commit
ada9562cc2
72 changed files with 954686 additions and 577372 deletions
30
alembic/versions/8c55f5602451_remove_entity_type_column.py
Normal file
30
alembic/versions/8c55f5602451_remove_entity_type_column.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
"""remove_entity_type_column
|
||||
|
||||
Revision ID: 8c55f5602451
|
||||
Revises: 2a76a5bc2f09
|
||||
Create Date: 2025-11-07 17:08:07.329740
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '8c55f5602451'
|
||||
down_revision: Union[str, Sequence[str], None] = '2a76a5bc2f09'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# Remove entity_type column from entities table
|
||||
op.execute("ALTER TABLE entities DROP COLUMN IF EXISTS entity_type")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# Re-add entity_type column (default to 'OTHER' for existing rows)
|
||||
op.execute("ALTER TABLE entities ADD COLUMN entity_type TEXT DEFAULT 'OTHER'")
|
||||
387809
benchmarks-results/longmemeval/final.json
Normal file
387809
benchmarks-results/longmemeval/final.json
Normal file
File diff suppressed because one or more lines are too long
BIN
benchmarks/.DS_Store
vendored
BIN
benchmarks/.DS_Store
vendored
Binary file not shown.
|
|
@ -19,6 +19,7 @@ The framework supports two answer generation patterns:
|
|||
|
||||
import json
|
||||
import asyncio
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Dict, Any, Optional, Tuple
|
||||
|
|
@ -91,11 +92,17 @@ class LLMAnswerGenerator(ABC):
|
|||
async def generate_answer(
|
||||
self,
|
||||
question: str,
|
||||
memories: List[Dict[str, Any]]
|
||||
memories: List[Dict[str, Any]],
|
||||
question_date: Optional[datetime] = None
|
||||
) -> Tuple[str, str, Optional[List[Dict[str, Any]]]]:
|
||||
"""
|
||||
Generate answer from retrieved memories.
|
||||
|
||||
Args:
|
||||
question: The question text
|
||||
memories: Retrieved memories to use for answering
|
||||
question_date: Optional date when the question was asked (for temporal context)
|
||||
|
||||
Returns:
|
||||
Tuple of (answer, reasoning, retrieved_memories_override)
|
||||
- answer: The generated answer text
|
||||
|
|
@ -166,6 +173,7 @@ Your task is to label an answer to a question as 'CORRECT' or 'WRONG'. You will
|
|||
The generated answer might be much longer, but you should be generous with your grading - as long as it touches on the same topic as the gold answer, it should be counted as CORRECT.
|
||||
|
||||
For time related questions, the gold answer will be a specific date, month, year, etc. The generated answer might be much longer or use relative time references (like "last Tuesday" or "next month"), but you should be generous with your grading - as long as it refers to the same date or time period as the gold answer, it should be counted as CORRECT. Even if the format differs (e.g., "May 7th" vs "7 May"), consider it CORRECT if it's the same date.
|
||||
There's an edge case where the actual answer can't be found in the data and in that case the gold answer will say so (e.g. 'You did not mention this information.'); if the generated answer says that it cannot be answered or it doesn't know, it should be counted as CORRECT.
|
||||
|
||||
Now it's time for the real question:
|
||||
Question: {question}
|
||||
|
|
@ -294,29 +302,39 @@ class BenchmarkRunner:
|
|||
question: str,
|
||||
thinking_budget: int = 500,
|
||||
max_tokens: int = 4096,
|
||||
question_date: Optional[datetime] = None,
|
||||
) -> Tuple[str, str, List[Dict]]:
|
||||
"""
|
||||
Answer a question using memory retrieval.
|
||||
|
||||
Args:
|
||||
agent_id: Agent ID
|
||||
question: Question text
|
||||
thinking_budget: Thinking budget for search
|
||||
max_tokens: Maximum tokens to retrieve
|
||||
question_date: Date when the question was asked (for temporal filtering)
|
||||
|
||||
Returns:
|
||||
Tuple of (answer, reasoning, retrieved_memories)
|
||||
"""
|
||||
# Check if generator needs external search
|
||||
if self.answer_generator.needs_external_search():
|
||||
# Traditional flow: search then generate
|
||||
# Search both 'world' and 'agent' fact types in parallel
|
||||
results, _ = await self.memory.search_async(
|
||||
agent_id=agent_id,
|
||||
query=question,
|
||||
thinking_budget=thinking_budget,
|
||||
max_tokens=max_tokens,
|
||||
fact_type="world"
|
||||
fact_type=["world", "agent"],
|
||||
question_date=question_date
|
||||
)
|
||||
|
||||
if not results:
|
||||
return "I don't have enough information to answer that question.", "No relevant memories found.", []
|
||||
|
||||
# Generate answer using LLM
|
||||
answer, reasoning, memories_override = await self.answer_generator.generate_answer(question, results)
|
||||
answer, reasoning, memories_override = await self.answer_generator.generate_answer(question, results, question_date)
|
||||
|
||||
# Use override if provided, otherwise use search results
|
||||
final_memories = memories_override if memories_override is not None else results
|
||||
|
|
@ -325,7 +343,7 @@ class BenchmarkRunner:
|
|||
else:
|
||||
# Integrated flow: generator does its own search (e.g., think API)
|
||||
# Pass empty memories list since generator doesn't need them
|
||||
answer, reasoning, memories_override = await self.answer_generator.generate_answer(question, [])
|
||||
answer, reasoning, memories_override = await self.answer_generator.generate_answer(question, [], question_date)
|
||||
|
||||
# Use memories from generator (should not be None for integrated mode)
|
||||
final_memories = memories_override if memories_override is not None else []
|
||||
|
|
@ -373,24 +391,32 @@ class BenchmarkRunner:
|
|||
question = qa['question']
|
||||
correct_answer = qa['answer']
|
||||
category = qa.get('category', 0)
|
||||
question_date = qa.get('question_date')
|
||||
|
||||
try:
|
||||
# Get predicted answer, reasoning, and retrieved memories
|
||||
predicted_answer, reasoning, retrieved_memories = await self.answer_question(
|
||||
agent_id, question, thinking_budget, max_tokens
|
||||
agent_id, question, thinking_budget, max_tokens, question_date
|
||||
)
|
||||
|
||||
# Remove embeddings from retrieved memories to reduce file size
|
||||
memories_without_embeddings = [
|
||||
{k: v for k, v in mem.items() if k != 'embedding'}
|
||||
for mem in retrieved_memories
|
||||
]
|
||||
|
||||
return {
|
||||
'question': question,
|
||||
'correct_answer': correct_answer,
|
||||
'predicted_answer': predicted_answer,
|
||||
'reasoning': reasoning,
|
||||
'category': category,
|
||||
'retrieved_memories': retrieved_memories,
|
||||
'retrieved_memories': memories_without_embeddings,
|
||||
'is_invalid': False,
|
||||
'error': None
|
||||
}
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
# Mark as invalid if answer generation failed
|
||||
console.print(f" [red]✗[/red] Failed to answer question: {str(e)[:100]}")
|
||||
return {
|
||||
|
|
@ -508,6 +534,38 @@ class BenchmarkRunner:
|
|||
'detailed_results': judged_results
|
||||
}
|
||||
|
||||
async def _agent_has_data(self, agent_id: str) -> bool:
|
||||
"""
|
||||
Check if an agent has any indexed memory units.
|
||||
|
||||
Args:
|
||||
agent_id: Agent ID to check
|
||||
|
||||
Returns:
|
||||
True if agent has at least one memory unit, False otherwise
|
||||
"""
|
||||
try:
|
||||
# Check if we're using a remote client or local memory
|
||||
from memora.remote_client import RemoteMemoryClient
|
||||
|
||||
if isinstance(self.memory, RemoteMemoryClient):
|
||||
# Use stats API for remote client
|
||||
stats = await self.memory.get_agent_stats(agent_id)
|
||||
total_nodes = stats.get("total_nodes", 0)
|
||||
return total_nodes > 0
|
||||
else:
|
||||
# Use direct database access for local memory
|
||||
pool = await self.memory._get_pool()
|
||||
async with pool.acquire() as conn:
|
||||
result = await conn.fetchrow(
|
||||
"SELECT COUNT(*) as count FROM memory_units WHERE agent_id = $1 LIMIT 1",
|
||||
agent_id
|
||||
)
|
||||
return result['count'] > 0
|
||||
except Exception as e:
|
||||
console.print(f" [red]Warning: Error checking agent data: {e}[/red]")
|
||||
return False
|
||||
|
||||
async def process_single_item(
|
||||
self,
|
||||
item: Dict,
|
||||
|
|
@ -520,10 +578,15 @@ class BenchmarkRunner:
|
|||
skip_ingestion: bool,
|
||||
question_semaphore: asyncio.Semaphore,
|
||||
eval_semaphore_size: int = 8,
|
||||
clear_this_agent: bool = True,
|
||||
) -> Dict:
|
||||
"""
|
||||
Process a single item (ingest + evaluate).
|
||||
|
||||
Args:
|
||||
clear_this_agent: Whether to clear this agent's data before ingesting.
|
||||
Set to False to skip clearing (e.g., when agent_id is shared and already cleared)
|
||||
|
||||
Returns:
|
||||
Result dict with metrics
|
||||
"""
|
||||
|
|
@ -532,8 +595,8 @@ class BenchmarkRunner:
|
|||
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:
|
||||
# Clear agent data before ingesting
|
||||
if clear_this_agent:
|
||||
console.print(" [1] Clearing previous agent data...")
|
||||
await self.memory.delete_agent(agent_id)
|
||||
console.print(f" [green]✓[/green] Cleared '{agent_id}' agent data")
|
||||
|
|
@ -579,11 +642,12 @@ class BenchmarkRunner:
|
|||
thinking_budget: int = 500,
|
||||
max_tokens: int = 4096,
|
||||
skip_ingestion: bool = False,
|
||||
max_concurrent_questions: int = 10, # Match search semaphore limit
|
||||
max_concurrent_questions: int = 1, # Default to 1 for sequential processing
|
||||
eval_semaphore_size: int = 8,
|
||||
clear_agent_per_item: bool = False,
|
||||
specific_item: Optional[str] = None,
|
||||
separate_ingestion_phase: bool = False,
|
||||
filln: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Run the full benchmark evaluation.
|
||||
|
|
@ -601,6 +665,7 @@ class BenchmarkRunner:
|
|||
clear_agent_per_item: Use unique agent ID per item for isolation (deprecated when separate_ingestion_phase=True)
|
||||
specific_item: If provided, only run this specific item ID (e.g., conversation)
|
||||
separate_ingestion_phase: If True, ingest all data first, then evaluate all questions (single agent)
|
||||
filln: If True, only process items where the agent has no indexed data yet
|
||||
|
||||
Returns:
|
||||
Dict with complete benchmark results
|
||||
|
|
@ -639,7 +704,7 @@ class BenchmarkRunner:
|
|||
items, agent_id, thinking_budget, max_tokens,
|
||||
skip_ingestion, max_questions_per_item,
|
||||
max_concurrent_questions, eval_semaphore_size,
|
||||
clear_agent_per_item
|
||||
clear_agent_per_item, filln
|
||||
)
|
||||
|
||||
async def _run_single_phase(
|
||||
|
|
@ -653,6 +718,7 @@ class BenchmarkRunner:
|
|||
max_concurrent_questions: int,
|
||||
eval_semaphore_size: int,
|
||||
clear_agent_per_item: bool,
|
||||
filln: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
"""Original single-phase approach: process each item independently."""
|
||||
# Create semaphore for question processing
|
||||
|
|
@ -664,12 +730,29 @@ class BenchmarkRunner:
|
|||
for i, item in enumerate(items, 1):
|
||||
# Use unique agent ID per item if requested (for isolation in benchmarks like LongMemEval)
|
||||
# This avoids deadlocks from deleting agent data
|
||||
item_agent_id = f"{agent_id}_item_{i-1}" if clear_agent_per_item else agent_id
|
||||
if clear_agent_per_item:
|
||||
item_id = self.dataset.get_item_id(item)
|
||||
item_agent_id = f"{agent_id}_{item_id}"
|
||||
# Always clear for unique agents (each agent_id is used only once)
|
||||
clear_this_agent = True
|
||||
else:
|
||||
item_agent_id = agent_id
|
||||
# Only clear on first item for shared agent_id
|
||||
clear_this_agent = (i == 1)
|
||||
|
||||
# Check if we should skip this item (filln mode)
|
||||
if filln:
|
||||
has_data = await self._agent_has_data(item_agent_id)
|
||||
if has_data:
|
||||
console.print(f"\n[bold blue]Item {i}/{len(items)}[/bold blue] (ID: {self.dataset.get_item_id(item)})")
|
||||
console.print(f" [yellow]⊘[/yellow] Skipping - agent '{item_agent_id}' already has indexed data")
|
||||
continue
|
||||
|
||||
result = await self.process_single_item(
|
||||
item, item_agent_id, i, len(items),
|
||||
thinking_budget, max_tokens, max_questions_per_item,
|
||||
skip_ingestion, question_semaphore, eval_semaphore_size,
|
||||
clear_this_agent,
|
||||
)
|
||||
all_results.append(result)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ Provides dataset, answer generator, and evaluator for the LoComo benchmark.
|
|||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from benchmarks.common.benchmark_runner import BenchmarkRunner
|
||||
from memora import TemporalSemanticMemory
|
||||
|
||||
# Add parent directory to path for imports
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||
|
||||
|
|
@ -124,11 +127,17 @@ class LoComoAnswerGenerator(LLMAnswerGenerator):
|
|||
async def generate_answer(
|
||||
self,
|
||||
question: str,
|
||||
memories: List[Dict[str, Any]]
|
||||
memories: List[Dict[str, Any]],
|
||||
question_date: Optional[datetime] = None
|
||||
) -> Tuple[str, str, Optional[List[Dict[str, Any]]]]:
|
||||
"""
|
||||
Generate answer from retrieved memories using Groq.
|
||||
|
||||
Args:
|
||||
question: The question text
|
||||
memories: Retrieved memories
|
||||
question_date: Date when the question was asked (for temporal context)
|
||||
|
||||
Returns:
|
||||
Tuple of (answer, reasoning, None)
|
||||
- None indicates to use the memories passed in
|
||||
|
|
@ -140,6 +149,11 @@ class LoComoAnswerGenerator(LLMAnswerGenerator):
|
|||
|
||||
context = json.dumps(context_parts)
|
||||
|
||||
# Format question date if provided
|
||||
question_date_str = ""
|
||||
if question_date:
|
||||
question_date_str = f"\n# CURRENT DATE:\nThe question is being asked on: {question_date.strftime('%Y-%m-%d %H:%M:%S')} UTC\n"
|
||||
|
||||
# Use LLM to generate answer
|
||||
try:
|
||||
answer_obj = await self.llm_config.call(
|
||||
|
|
@ -153,7 +167,7 @@ class LoComoAnswerGenerator(LLMAnswerGenerator):
|
|||
"content": f"""
|
||||
# CONTEXT:
|
||||
You have access to facts and entities from a conversation.
|
||||
|
||||
{question_date_str}
|
||||
# INSTRUCTIONS:
|
||||
1. Carefully analyze all provided memories
|
||||
2. Pay special attention to the timestamps to determine the answer
|
||||
|
|
@ -230,7 +244,8 @@ class LoComoThinkAnswerGenerator(LLMAnswerGenerator):
|
|||
async def generate_answer(
|
||||
self,
|
||||
question: str,
|
||||
memories: List[Dict[str, Any]]
|
||||
memories: List[Dict[str, Any]],
|
||||
question_date: Optional[datetime] = None
|
||||
) -> Tuple[str, str, Optional[List[Dict[str, Any]]]]:
|
||||
"""
|
||||
Generate answer using the integrated think API.
|
||||
|
|
@ -241,6 +256,7 @@ class LoComoThinkAnswerGenerator(LLMAnswerGenerator):
|
|||
Args:
|
||||
question: Question to answer
|
||||
memories: Not used (empty list), as think does its own retrieval
|
||||
question_date: Date when the question was asked (currently not used by think API)
|
||||
|
||||
Returns:
|
||||
Tuple of (answer, reasoning, retrieved_memories)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,16 +1,7 @@
|
|||
# LoComo Benchmark Results
|
||||
|
||||
**Overall Accuracy**: 73.50% (1029/1404)
|
||||
**Overall Accuracy**: 70.67% (106/150)
|
||||
|
||||
| Sample ID | Sessions | Questions | Correct | Accuracy | Multi-hop | Single-hop | Temporal | Open-domain |
|
||||
|-----------|----------|-----------|---------|----------|-----------|------------|----------|-------------|
|
||||
| conv-26 | 19 | 150 | 107 | 71.33% | N/A | N/A | N/A | N/A |
|
||||
| conv-30 | 19 | 81 | 64 | 79.01% | N/A | N/A | N/A | N/A |
|
||||
| conv-41 | 32 | 150 | 114 | 76.00% | N/A | N/A | N/A | N/A |
|
||||
| conv-42 | 29 | 150 | 103 | 68.67% | N/A | N/A | N/A | N/A |
|
||||
| conv-43 | 29 | 150 | 111 | 74.00% | N/A | N/A | N/A | N/A |
|
||||
| conv-44 | 28 | 123 | 96 | 78.05% | N/A | N/A | N/A | N/A |
|
||||
| conv-47 | 31 | 150 | 105 | 70.95% | N/A | N/A | N/A | N/A |
|
||||
| conv-48 | 30 | 150 | 116 | 77.85% | N/A | N/A | N/A | N/A |
|
||||
| conv-49 | 25 | 150 | 106 | 70.67% | N/A | N/A | N/A | N/A |
|
||||
| conv-50 | 30 | 150 | 107 | 71.81% | N/A | N/A | N/A | N/A |
|
||||
| conv-26 | 19 | 150 | 106 | 70.67% | N/A | N/A | N/A | N/A |
|
||||
|
|
@ -101,12 +101,18 @@ class LongMemEvalDataset(BenchmarkDataset):
|
|||
For LongMemEval, each item has one question.
|
||||
|
||||
Returns:
|
||||
List with single QA dict with 'question', 'answer', 'category'
|
||||
List with single QA dict with 'question', 'answer', 'category', 'question_date'
|
||||
"""
|
||||
# Parse question_date if available
|
||||
question_date = None
|
||||
if 'question_date' in item:
|
||||
question_date = self._parse_date(item['question_date'])
|
||||
|
||||
return [{
|
||||
'question': item.get("question", ""),
|
||||
'answer': item.get("answer", ""),
|
||||
'category': item.get("question_type", "unknown")
|
||||
'category': item.get("question_type", "unknown"),
|
||||
'question_date': question_date
|
||||
}]
|
||||
|
||||
def _parse_date(self, date_str: str) -> datetime:
|
||||
|
|
@ -146,11 +152,17 @@ class LongMemEvalAnswerGenerator(LLMAnswerGenerator):
|
|||
async def generate_answer(
|
||||
self,
|
||||
question: str,
|
||||
memories: List[Dict[str, Any]]
|
||||
memories: List[Dict[str, Any]],
|
||||
question_date: Optional[datetime] = None
|
||||
) -> Tuple[str, str, Optional[List[Dict[str, Any]]]]:
|
||||
"""
|
||||
Generate answer from retrieved memories using Groq.
|
||||
|
||||
Args:
|
||||
question: The question text
|
||||
memories: Retrieved memories
|
||||
question_date: Date when the question was asked (for temporal context)
|
||||
|
||||
Returns:
|
||||
Tuple of (answer, reasoning, None)
|
||||
- None indicates to use the memories passed in
|
||||
|
|
@ -163,6 +175,11 @@ class LongMemEvalAnswerGenerator(LLMAnswerGenerator):
|
|||
|
||||
context = json.dumps(context_parts)
|
||||
|
||||
# Format question date if provided
|
||||
question_date_str = ""
|
||||
if question_date:
|
||||
question_date_str = f"\n# CURRENT DATE:\nThe question is being asked on: {question_date.strftime('%Y-%m-%d %H:%M:%S')} UTC\n"
|
||||
|
||||
# Use LLM to generate answer
|
||||
try:
|
||||
answer_obj = await self.llm_config.call(
|
||||
|
|
@ -176,7 +193,7 @@ class LongMemEvalAnswerGenerator(LLMAnswerGenerator):
|
|||
"content": f"""
|
||||
# CONTEXT:
|
||||
You have access to facts and entities from a conversation.
|
||||
|
||||
{question_date_str}
|
||||
# INSTRUCTIONS:
|
||||
1. Carefully analyze all provided memories
|
||||
2. Pay special attention to the timestamps to determine the answer
|
||||
|
|
@ -208,6 +225,8 @@ class LongMemEvalAnswerGenerator(LLMAnswerGenerator):
|
|||
7. Ensure your final answer is specific and avoids vague time references
|
||||
8. If you're not exactly sure, still try to attempt an answer. Sometimes the terms are sligtly different from the question, so it's better to try with the current evidence than just say you don't know.
|
||||
9. Say that you cannot answer if no evidence is related to the question.
|
||||
10. Instead of saying "I don't know", you can use the most relevant information you found in the memories to construct a best-effort answer (but you need to use the provided context).
|
||||
11. Provide a complete answer with your reasoning.
|
||||
|
||||
Context:
|
||||
|
||||
|
|
@ -230,10 +249,14 @@ class LongMemEvalAnswerGenerator(LLMAnswerGenerator):
|
|||
async def run_benchmark(
|
||||
max_instances: int = None,
|
||||
max_questions_per_instance: int = None,
|
||||
thinking_budget: int = 100,
|
||||
max_tokens: int = 4096,
|
||||
thinking_budget: int = 500,
|
||||
max_tokens: int = 8192,
|
||||
skip_ingestion: bool = False,
|
||||
api_url: str = None
|
||||
api_url: str = None,
|
||||
filln: bool = False,
|
||||
question_id: str = None,
|
||||
only_failed: bool = False,
|
||||
only_invalid: bool = False
|
||||
):
|
||||
"""
|
||||
Run the LongMemEval benchmark.
|
||||
|
|
@ -245,6 +268,10 @@ async def run_benchmark(
|
|||
max_tokens: Maximum tokens to retrieve from memories
|
||||
skip_ingestion: Whether to skip ingestion and use existing data
|
||||
api_url: Optional API URL to connect to (default: use local memory)
|
||||
filln: If True, only process questions where the agent has no indexed data yet
|
||||
question_id: Optional question ID to filter (e.g., 'e47becba'). Useful with --skip-ingestion.
|
||||
only_failed: If True, only run questions that were previously marked as incorrect (is_correct=False)
|
||||
only_invalid: If True, only run questions that were previously marked as invalid (is_invalid=True)
|
||||
"""
|
||||
from rich.console import Console
|
||||
console = Console()
|
||||
|
|
@ -257,8 +284,49 @@ async def run_benchmark(
|
|||
console.print("[yellow]curl -L 'https://huggingface.co/datasets/xiaowu0162/longmemeval-cleaned/resolve/main/longmemeval_s_cleaned.json' -o benchmarks/longmemeval/datasets/longmemeval_s_cleaned.json[/yellow]")
|
||||
return
|
||||
|
||||
# Load previous results if filtering for failed/invalid questions
|
||||
failed_question_ids = set()
|
||||
invalid_question_ids = set()
|
||||
if only_failed or only_invalid:
|
||||
results_path = Path(__file__).parent / 'results' / 'benchmark_results.json'
|
||||
if not results_path.exists():
|
||||
console.print(f"[red]Error: Cannot use --only-failed or --only-invalid without existing results file[/red]")
|
||||
console.print(f"[yellow]Results file not found: {results_path}[/yellow]")
|
||||
return
|
||||
|
||||
with open(results_path, 'r') as f:
|
||||
previous_results = json.load(f)
|
||||
|
||||
# Extract question IDs that failed or are invalid
|
||||
for item_result in previous_results.get('item_results', []):
|
||||
item_id = item_result['item_id']
|
||||
for detail in item_result['metrics'].get('detailed_results', []):
|
||||
if only_failed and detail.get('is_correct') == False and not detail.get('is_invalid', False):
|
||||
failed_question_ids.add(item_id)
|
||||
if only_invalid and detail.get('is_invalid', False):
|
||||
invalid_question_ids.add(item_id)
|
||||
|
||||
if only_failed:
|
||||
console.print(f"[cyan]Filtering to {len(failed_question_ids)} questions that failed (is_correct=False)[/cyan]")
|
||||
if only_invalid:
|
||||
console.print(f"[cyan]Filtering to {len(invalid_question_ids)} questions that were invalid (is_invalid=True)[/cyan]")
|
||||
|
||||
# Initialize components
|
||||
dataset = LongMemEvalDataset()
|
||||
|
||||
# Filter dataset based on failed/invalid flags
|
||||
if only_failed or only_invalid:
|
||||
target_ids = failed_question_ids if only_failed else invalid_question_ids
|
||||
if not target_ids:
|
||||
filter_type = "failed" if only_failed else "invalid"
|
||||
console.print(f"[yellow]No {filter_type} questions found in previous results. Nothing to run.[/yellow]")
|
||||
return
|
||||
# Override question_id to be None if we're filtering by failed/invalid
|
||||
# The filtering will happen when we load the dataset
|
||||
original_dataset_items = dataset.load(dataset_path, max_instances)
|
||||
filtered_items = [item for item in original_dataset_items if dataset.get_item_id(item) in target_ids]
|
||||
console.print(f"[green]Found {len(filtered_items)} items to re-evaluate[/green]")
|
||||
|
||||
answer_generator = LongMemEvalAnswerGenerator()
|
||||
answer_evaluator = LLMAnswerEvaluator()
|
||||
|
||||
|
|
@ -283,12 +351,20 @@ async def run_benchmark(
|
|||
memory=memory
|
||||
)
|
||||
|
||||
# If filtering by failed/invalid, we need to use a custom dataset that only returns those items
|
||||
# We'll temporarily replace the dataset's load method
|
||||
if only_failed or only_invalid:
|
||||
original_load = dataset.load
|
||||
def filtered_load(path: Path, max_items: Optional[int] = None):
|
||||
return filtered_items[:max_items] if max_items else filtered_items
|
||||
dataset.load = filtered_load
|
||||
|
||||
# Run benchmark
|
||||
# Two-phase approach: ingest all 500 conversations into single agent, then evaluate all questions
|
||||
# This is more realistic and tests retrieval from a large memory base
|
||||
# Single-phase approach: each question gets its own isolated agent_id
|
||||
# This ensures each question only has access to its own context
|
||||
results = await runner.run(
|
||||
dataset_path=dataset_path,
|
||||
agent_id="longmemeval",
|
||||
agent_id="longmemeval", # Will be suffixed with question_id per item
|
||||
max_items=max_instances,
|
||||
max_questions_per_item=max_questions_per_instance,
|
||||
thinking_budget=thinking_budget,
|
||||
|
|
@ -296,12 +372,19 @@ async def run_benchmark(
|
|||
skip_ingestion=skip_ingestion,
|
||||
max_concurrent_questions=8,
|
||||
eval_semaphore_size=8,
|
||||
separate_ingestion_phase=True # Ingest all data first, then evaluate all questions
|
||||
separate_ingestion_phase=False, # Process each question independently
|
||||
clear_agent_per_item=True, # Use unique agent_id per question
|
||||
filln=filln, # Only process questions without indexed data
|
||||
specific_item=question_id # Optional filter for specific question ID
|
||||
)
|
||||
|
||||
# Display and save results
|
||||
runner.display_results(results)
|
||||
runner.save_results(results, Path(__file__).parent / 'results' / 'benchmark_results.json')
|
||||
runner.save_results(
|
||||
results,
|
||||
Path(__file__).parent / 'results' / 'benchmark_results.json',
|
||||
merge_with_existing=(filln or question_id is not None or only_failed or only_invalid) # Merge when using --fill, --only-failed, --only-invalid flags or specific question
|
||||
)
|
||||
|
||||
# Generate detailed report by question type
|
||||
generate_type_report(results)
|
||||
|
|
@ -414,13 +497,13 @@ if __name__ == "__main__":
|
|||
parser.add_argument(
|
||||
"--thinking-budget",
|
||||
type=int,
|
||||
default=100,
|
||||
default=500,
|
||||
help="Thinking budget for spreading activation search"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-tokens",
|
||||
type=int,
|
||||
default=4096,
|
||||
default=8192,
|
||||
help="Maximum tokens to retrieve from memories"
|
||||
)
|
||||
parser.add_argument(
|
||||
|
|
@ -434,14 +517,43 @@ if __name__ == "__main__":
|
|||
default=None,
|
||||
help="Memora API URL (default: use local memory, example: http://localhost:8000)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--fill",
|
||||
action="store_true",
|
||||
help="Only process questions where the agent has no indexed data yet (for resuming interrupted runs)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--question-id",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Filter to specific question ID (e.g., 'e47becba'). Useful with --skip-ingestion to test a single question."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--only-failed",
|
||||
action="store_true",
|
||||
help="Only run questions that were previously marked as incorrect (is_correct=False). Requires existing results file."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--only-invalid",
|
||||
action="store_true",
|
||||
help="Only run questions that were previously marked as invalid (is_invalid=True). Requires existing results file."
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Validate that only one of --only-failed or --only-invalid is set
|
||||
if args.only_failed and args.only_invalid:
|
||||
parser.error("Cannot use both --only-failed and --only-invalid at the same time")
|
||||
|
||||
results = asyncio.run(run_benchmark(
|
||||
max_instances=args.max_instances,
|
||||
max_questions_per_instance=args.max_questions,
|
||||
thinking_budget=args.thinking_budget,
|
||||
max_tokens=args.max_tokens,
|
||||
skip_ingestion=args.skip_ingestion,
|
||||
api_url=args.api_url
|
||||
api_url=args.api_url,
|
||||
filln=args.fill,
|
||||
question_id=args.question_id,
|
||||
only_failed=args.only_failed,
|
||||
only_invalid=args.only_invalid
|
||||
))
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
913
benchmarks/visualizer/main.py
Normal file
913
benchmarks/visualizer/main.py
Normal file
|
|
@ -0,0 +1,913 @@
|
|||
"""Benchmark Visualizer - FastHTML App
|
||||
|
||||
A fast web interface for visualizing benchmark results.
|
||||
Supports LoComo and LongMemEval benchmark visualization.
|
||||
|
||||
Usage:
|
||||
python main.py
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from fasthtml.common import *
|
||||
|
||||
# Get the benchmarks directory
|
||||
BENCHMARKS_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
# Tailwind + shadcn/ui theme
|
||||
def get_head():
|
||||
return (
|
||||
Script(src="https://cdn.tailwindcss.com"),
|
||||
Script("""
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(214.3 31.8% 91.4%)",
|
||||
input: "hsl(214.3 31.8% 91.4%)",
|
||||
ring: "hsl(222.2 84% 4.9%)",
|
||||
background: "hsl(0 0% 100%)",
|
||||
foreground: "hsl(222.2 84% 4.9%)",
|
||||
primary: {
|
||||
DEFAULT: "hsl(222.2 47.4% 11.2%)",
|
||||
foreground: "hsl(210 40% 98%)",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(210 40% 96.1%)",
|
||||
foreground: "hsl(222.2 47.4% 11.2%)",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(0 84.2% 60.2%)",
|
||||
foreground: "hsl(210 40% 98%)",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(210 40% 96.1%)",
|
||||
foreground: "hsl(215.4 16.3% 46.9%)",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(210 40% 96.1%)",
|
||||
foreground: "hsl(222.2 47.4% 11.2%)",
|
||||
},
|
||||
success: {
|
||||
DEFAULT: "hsl(142.1 76.2% 36.3%)",
|
||||
foreground: "hsl(355.7 100% 97.3%)",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "0.5rem",
|
||||
md: "calc(0.5rem - 2px)",
|
||||
sm: "calc(0.5rem - 4px)",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""),
|
||||
Style("""
|
||||
@layer base {
|
||||
* { border-color: hsl(var(--border)); }
|
||||
body {
|
||||
background-color: hsl(210 40% 96.1%);
|
||||
color: hsl(222.2 84% 4.9%);
|
||||
}
|
||||
}
|
||||
""")
|
||||
)
|
||||
|
||||
# Create FastHTML app
|
||||
app, rt = fast_app()
|
||||
|
||||
|
||||
def load_locomo_results(mode: str = "search") -> dict[str, Any] | None:
|
||||
"""Load LoComo benchmark results."""
|
||||
filename = "benchmark_results_think.json" if mode == "think" else "benchmark_results.json"
|
||||
results_path = BENCHMARKS_DIR / "locomo" / "results" / filename
|
||||
|
||||
if not results_path.exists():
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(results_path) as f:
|
||||
return json.load(f)
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
|
||||
|
||||
def load_longmemeval_results() -> dict[str, Any] | None:
|
||||
"""Load LongMemEval benchmark results."""
|
||||
results_path = BENCHMARKS_DIR / "longmemeval" / "results" / "benchmark_results.json"
|
||||
|
||||
if not results_path.exists():
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(results_path) as f:
|
||||
return json.load(f)
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
|
||||
|
||||
def get_category_name(category: int | str) -> str:
|
||||
"""Map category ID to name for LoComo."""
|
||||
if isinstance(category, str):
|
||||
return category
|
||||
|
||||
categories = {
|
||||
1: "Multi-hop",
|
||||
2: "Single-hop",
|
||||
3: "Temporal",
|
||||
4: "Open-domain"
|
||||
}
|
||||
return categories.get(category, "Unknown")
|
||||
|
||||
|
||||
@rt("/")
|
||||
def get():
|
||||
"""Main page."""
|
||||
return (
|
||||
Title("Benchmark Visualizer"),
|
||||
get_head(),
|
||||
Main(
|
||||
Div(
|
||||
H1("📊 Benchmark Visualizer", cls="text-4xl font-bold text-foreground"),
|
||||
P("Analyze and visualize benchmark results", cls="text-muted-foreground mt-2"),
|
||||
cls="text-center py-12"
|
||||
),
|
||||
Div(
|
||||
Label("Select a benchmark to view:", cls="block text-sm font-medium text-foreground mb-2"),
|
||||
Select(
|
||||
Option("-- Choose a benchmark --", value="", selected=True),
|
||||
Option("LoComo (search mode)", value="/locomo/search"),
|
||||
Option("LoComo (think mode)", value="/locomo/think"),
|
||||
Option("LongMemEval", value="/longmemeval"),
|
||||
onchange="if(this.value) window.location.href = this.value;",
|
||||
cls="w-full px-3 py-2 border border-input rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
),
|
||||
cls="max-w-md mx-auto bg-white border border-border rounded-lg p-6 shadow-sm"
|
||||
),
|
||||
cls="container mx-auto max-w-7xl px-4 py-8"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@rt("/locomo/{mode}")
|
||||
def get_locomo(mode: str, filter_type: str = "all", category_filter: str = "all"):
|
||||
"""Render LoComo results."""
|
||||
data = load_locomo_results(mode)
|
||||
|
||||
if not data:
|
||||
mode_label = "think" if mode == "think" else "search"
|
||||
extra_args = " --use-think" if mode == "think" else ""
|
||||
return (
|
||||
Title(f"LoComo ({mode_label}) - Not Found"),
|
||||
get_head(),
|
||||
Main(
|
||||
H1("⚠️ Benchmark Results Not Found", cls="text-3xl font-bold text-foreground mb-4"),
|
||||
P(f"The {mode_label} mode results are not available.", cls="text-muted-foreground mb-6"),
|
||||
H4("To generate results:", cls="text-lg font-semibold text-foreground mb-2"),
|
||||
Pre(f"./scripts/benchmarks/run-locomo.sh --env local{extra_args}", cls="bg-slate-900 text-slate-100 p-4 rounded-md overflow-x-auto text-sm"),
|
||||
A("← Back", href="/", cls="inline-flex items-center mt-6 px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 text-sm font-medium"),
|
||||
cls="container mx-auto max-w-7xl px-4 py-8"
|
||||
)
|
||||
)
|
||||
|
||||
all_results = data.get("item_results", data.get("conversation_results", []))
|
||||
|
||||
# Filter items based on their questions
|
||||
results = []
|
||||
for item in all_results:
|
||||
detailed_results = item.get("metrics", {}).get("detailed_results", [])
|
||||
|
||||
# Apply correctness filter
|
||||
passes_correctness_filter = False
|
||||
if filter_type == "all":
|
||||
passes_correctness_filter = True
|
||||
elif filter_type == "correct":
|
||||
# Show items where all questions are correct (and not invalid)
|
||||
passes_correctness_filter = detailed_results and all(r.get("is_correct") and not r.get("is_invalid") for r in detailed_results)
|
||||
elif filter_type == "incorrect":
|
||||
# Show items that have at least one incorrect question
|
||||
passes_correctness_filter = any(not r.get("is_correct") and not r.get("is_invalid") for r in detailed_results)
|
||||
elif filter_type == "invalid":
|
||||
# Show items that have at least one invalid question
|
||||
passes_correctness_filter = any(r.get("is_invalid") for r in detailed_results)
|
||||
|
||||
# Apply category filter
|
||||
passes_category_filter = False
|
||||
if category_filter == "all":
|
||||
passes_category_filter = True
|
||||
else:
|
||||
# Show items that have at least one question of the specified category
|
||||
category_id = int(category_filter)
|
||||
passes_category_filter = any(r.get("category") == category_id for r in detailed_results)
|
||||
|
||||
if passes_correctness_filter and passes_category_filter:
|
||||
results.append(item)
|
||||
|
||||
# Calculate stats (use all_results for overall stats, not filtered results)
|
||||
category_stats = {
|
||||
1: {"name": "Multi-hop", "correct": 0, "total": 0, "invalid": 0},
|
||||
2: {"name": "Single-hop", "correct": 0, "total": 0, "invalid": 0},
|
||||
3: {"name": "Temporal", "correct": 0, "total": 0, "invalid": 0},
|
||||
4: {"name": "Open-domain", "correct": 0, "total": 0, "invalid": 0}
|
||||
}
|
||||
|
||||
total_invalid = 0
|
||||
for item in all_results:
|
||||
if item.get("metrics", {}).get("detailed_results"):
|
||||
for result in item["metrics"]["detailed_results"]:
|
||||
category = result.get("category")
|
||||
if category in category_stats:
|
||||
category_stats[category]["total"] += 1
|
||||
if result.get("is_invalid"):
|
||||
category_stats[category]["invalid"] += 1
|
||||
total_invalid += 1
|
||||
elif result.get("is_correct"):
|
||||
category_stats[category]["correct"] += 1
|
||||
|
||||
mode_label = " (Think Mode)" if mode == "think" else " (Search Mode)"
|
||||
|
||||
# Overall stats
|
||||
stats_html = Div(
|
||||
H3(f"LoComo Benchmark{mode_label} - Overall Performance", cls="text-2xl font-bold text-foreground mb-6"),
|
||||
Div(
|
||||
Div(
|
||||
P("Overall Accuracy", cls="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2"),
|
||||
P(f"{data['overall_accuracy']:.2f}%", cls="text-3xl font-bold text-foreground"),
|
||||
cls="bg-white border border-border rounded-lg p-6 text-center shadow-sm"
|
||||
),
|
||||
Div(
|
||||
P("Correct Answers", cls="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2"),
|
||||
P(f"{data['total_correct']} / {data['total_questions']}", cls="text-3xl font-bold text-foreground"),
|
||||
cls="bg-white border border-border rounded-lg p-6 text-center shadow-sm"
|
||||
),
|
||||
Div(
|
||||
P("Invalid Questions", cls="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2"),
|
||||
P(str(total_invalid), cls="text-3xl font-bold text-foreground"),
|
||||
cls="bg-white border border-border rounded-lg p-6 text-center shadow-sm"
|
||||
) if total_invalid > 0 else None,
|
||||
Div(
|
||||
P("Items", cls="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2"),
|
||||
P(str(len(all_results)), cls="text-3xl font-bold text-foreground"),
|
||||
cls="bg-white border border-border rounded-lg p-6 text-center shadow-sm"
|
||||
),
|
||||
cls="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8"
|
||||
),
|
||||
H4("Accuracy by Category", cls="text-xl font-semibold text-foreground mb-4"),
|
||||
Div(
|
||||
*[
|
||||
Div(
|
||||
P(cat["name"], cls="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2"),
|
||||
P(f"{(cat['correct'] / (cat['total'] - cat['invalid']) * 100) if (cat['total'] - cat['invalid']) > 0 else 0:.1f}%", cls="text-2xl font-bold text-foreground"),
|
||||
P(f"{cat['correct']} / {cat['total']}", cls="text-sm text-muted-foreground mt-1"),
|
||||
cls="bg-white border border-border rounded-lg p-6 text-center shadow-sm"
|
||||
)
|
||||
for cat in category_stats.values()
|
||||
],
|
||||
cls="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"
|
||||
)
|
||||
)
|
||||
|
||||
# Filter controls
|
||||
filters = Div(
|
||||
# Correctness filter
|
||||
Div(
|
||||
P("Filter by correctness:", cls="text-sm font-medium text-foreground mb-2"),
|
||||
Div(
|
||||
A("All", href=f"/locomo/{mode}?filter_type=all&category_filter={category_filter}",
|
||||
cls="px-3 py-1.5 rounded-md text-sm font-medium " + ("bg-primary text-primary-foreground" if filter_type == 'all' else "bg-white text-foreground border border-border hover:bg-accent")),
|
||||
A("✅ All Correct", href=f"/locomo/{mode}?filter_type=correct&category_filter={category_filter}",
|
||||
cls="px-3 py-1.5 rounded-md text-sm font-medium " + ("bg-primary text-primary-foreground" if filter_type == 'correct' else "bg-white text-foreground border border-border hover:bg-accent")),
|
||||
A("❌ Has Incorrect", href=f"/locomo/{mode}?filter_type=incorrect&category_filter={category_filter}",
|
||||
cls="px-3 py-1.5 rounded-md text-sm font-medium " + ("bg-primary text-primary-foreground" if filter_type == 'incorrect' else "bg-white text-foreground border border-border hover:bg-accent")),
|
||||
A("⚠️ Has Invalid", href=f"/locomo/{mode}?filter_type=invalid&category_filter={category_filter}",
|
||||
cls="px-3 py-1.5 rounded-md text-sm font-medium " + ("bg-primary text-primary-foreground" if filter_type == 'invalid' else "bg-white text-foreground border border-border hover:bg-accent")) if total_invalid > 0 else None,
|
||||
cls="flex flex-wrap gap-2"
|
||||
),
|
||||
cls="mb-4"
|
||||
),
|
||||
# Category filter
|
||||
Div(
|
||||
P("Filter by question category:", cls="text-sm font-medium text-foreground mb-2"),
|
||||
Div(
|
||||
A("All Categories", href=f"/locomo/{mode}?filter_type={filter_type}&category_filter=all",
|
||||
cls="px-3 py-1.5 rounded-md text-sm font-medium " + ("bg-primary text-primary-foreground" if category_filter == 'all' else "bg-white text-foreground border border-border hover:bg-accent")),
|
||||
A("Multi-hop", href=f"/locomo/{mode}?filter_type={filter_type}&category_filter=1",
|
||||
cls="px-3 py-1.5 rounded-md text-sm font-medium " + ("bg-primary text-primary-foreground" if category_filter == '1' else "bg-white text-foreground border border-border hover:bg-accent")),
|
||||
A("Single-hop", href=f"/locomo/{mode}?filter_type={filter_type}&category_filter=2",
|
||||
cls="px-3 py-1.5 rounded-md text-sm font-medium " + ("bg-primary text-primary-foreground" if category_filter == '2' else "bg-white text-foreground border border-border hover:bg-accent")),
|
||||
A("Temporal", href=f"/locomo/{mode}?filter_type={filter_type}&category_filter=3",
|
||||
cls="px-3 py-1.5 rounded-md text-sm font-medium " + ("bg-primary text-primary-foreground" if category_filter == '3' else "bg-white text-foreground border border-border hover:bg-accent")),
|
||||
A("Open-domain", href=f"/locomo/{mode}?filter_type={filter_type}&category_filter=4",
|
||||
cls="px-3 py-1.5 rounded-md text-sm font-medium " + ("bg-primary text-primary-foreground" if category_filter == '4' else "bg-white text-foreground border border-border hover:bg-accent")),
|
||||
cls="flex flex-wrap gap-2"
|
||||
),
|
||||
cls="mb-4"
|
||||
),
|
||||
cls="mb-6"
|
||||
)
|
||||
|
||||
# Render items
|
||||
items_html = []
|
||||
for item in results:
|
||||
# Find the original index in all_results
|
||||
original_idx = all_results.index(item)
|
||||
item_id = item.get("item_id", item.get("sample_id", f"item-{original_idx}"))
|
||||
metrics = item.get("metrics", {})
|
||||
accuracy = metrics.get("accuracy", 0)
|
||||
correct = metrics.get("correct", 0)
|
||||
total = metrics.get("total", 0)
|
||||
|
||||
color = "🟢" if accuracy >= 70 else ("🟡" if accuracy >= 50 else "🔴")
|
||||
|
||||
# Determine border color based on accuracy
|
||||
if accuracy >= 70:
|
||||
border_class = "border-l-4 border-green-600"
|
||||
bg_class = "hover:bg-green-50"
|
||||
elif accuracy >= 50:
|
||||
border_class = "border-l-4 border-yellow-500"
|
||||
bg_class = "hover:bg-yellow-50"
|
||||
else:
|
||||
border_class = "border-l-4 border-red-600"
|
||||
bg_class = "hover:bg-red-50"
|
||||
|
||||
# Show preview with link to detail page
|
||||
items_html.append(
|
||||
A(
|
||||
Div(
|
||||
Div(
|
||||
P(f"{color} {item_id}", cls="text-lg font-semibold text-foreground mb-2"),
|
||||
Div(
|
||||
Div(
|
||||
P("Accuracy", cls="text-xs font-medium text-muted-foreground uppercase tracking-wide"),
|
||||
P(f"{accuracy:.1f}%", cls="text-2xl font-bold text-foreground"),
|
||||
cls="text-center"
|
||||
),
|
||||
Div(
|
||||
P("Correct", cls="text-xs font-medium text-muted-foreground uppercase tracking-wide"),
|
||||
P(f"{correct}/{total}", cls="text-xl font-semibold text-foreground"),
|
||||
cls="text-center"
|
||||
),
|
||||
cls="flex gap-6 items-center"
|
||||
),
|
||||
cls="p-6"
|
||||
),
|
||||
cls=f"bg-white border border-border rounded-lg shadow-sm transition-all {border_class} {bg_class}"
|
||||
),
|
||||
href=f"/locomo/{mode}/item/{original_idx}?filter_type={filter_type}&category_filter={category_filter}",
|
||||
cls="block no-underline"
|
||||
)
|
||||
)
|
||||
|
||||
return (
|
||||
Title(f"LoComo ({mode_label})"),
|
||||
get_head(),
|
||||
Main(
|
||||
Div(
|
||||
A("← Back to benchmarks", href="/", cls="inline-flex items-center px-4 py-2 bg-white border border-border rounded-md text-sm font-medium text-foreground hover:bg-accent mb-6"),
|
||||
stats_html,
|
||||
Hr(cls="my-6 border-border"),
|
||||
filters,
|
||||
P(f"Showing {len(results)} of {len(all_results)} items", cls="text-sm text-muted-foreground mb-6"),
|
||||
Div(
|
||||
*items_html,
|
||||
cls="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"
|
||||
),
|
||||
cls="container mx-auto max-w-7xl px-4 py-8"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@rt("/locomo/{mode}/item/{item_idx}")
|
||||
def get_locomo_item(mode: str, item_idx: int, filter_type: str = "all"):
|
||||
"""Render a single LoComo item with questions."""
|
||||
data = load_locomo_results(mode)
|
||||
if not data:
|
||||
return Redirect("/")
|
||||
|
||||
results = data.get("item_results", data.get("conversation_results", []))
|
||||
if item_idx >= len(results):
|
||||
return Redirect(f"/locomo/{mode}")
|
||||
|
||||
item = results[item_idx]
|
||||
item_id = item.get("item_id", item.get("sample_id", f"item-{item_idx}"))
|
||||
metrics = item.get("metrics", {})
|
||||
accuracy = metrics.get("accuracy", 0)
|
||||
detailed_results = metrics.get("detailed_results", [])
|
||||
|
||||
# Filter questions
|
||||
filtered_questions = []
|
||||
for q_idx, result in enumerate(detailed_results):
|
||||
is_invalid = result.get("is_invalid", False)
|
||||
is_correct = result.get("is_correct", False)
|
||||
|
||||
if filter_type == "all":
|
||||
filtered_questions.append((q_idx, result))
|
||||
elif filter_type == "correct" and is_correct and not is_invalid:
|
||||
filtered_questions.append((q_idx, result))
|
||||
elif filter_type == "incorrect" and not is_correct and not is_invalid:
|
||||
filtered_questions.append((q_idx, result))
|
||||
elif filter_type == "invalid" and is_invalid:
|
||||
filtered_questions.append((q_idx, result))
|
||||
|
||||
# Filters for questions
|
||||
has_invalid = any(r.get("is_invalid", False) for r in detailed_results)
|
||||
q_filters = Div(
|
||||
P("Filter:", cls="text-sm font-medium text-foreground mb-2"),
|
||||
Div(
|
||||
A("All", href=f"/locomo/{mode}/item/{item_idx}?filter_type=all",
|
||||
cls="px-3 py-1.5 rounded-md text-sm font-medium " + ("bg-primary text-primary-foreground" if filter_type == 'all' else "bg-white text-foreground border border-border hover:bg-accent")),
|
||||
A("✅ Correct", href=f"/locomo/{mode}/item/{item_idx}?filter_type=correct",
|
||||
cls="px-3 py-1.5 rounded-md text-sm font-medium " + ("bg-primary text-primary-foreground" if filter_type == 'correct' else "bg-white text-foreground border border-border hover:bg-accent")),
|
||||
A("❌ Incorrect", href=f"/locomo/{mode}/item/{item_idx}?filter_type=incorrect",
|
||||
cls="px-3 py-1.5 rounded-md text-sm font-medium " + ("bg-primary text-primary-foreground" if filter_type == 'incorrect' else "bg-white text-foreground border border-border hover:bg-accent")),
|
||||
A("⚠️ Invalid", href=f"/locomo/{mode}/item/{item_idx}?filter_type=invalid",
|
||||
cls="px-3 py-1.5 rounded-md text-sm font-medium " + ("bg-primary text-primary-foreground" if filter_type == 'invalid' else "bg-white text-foreground border border-border hover:bg-accent")) if has_invalid else None,
|
||||
cls="flex flex-wrap gap-2"
|
||||
),
|
||||
cls="mb-6"
|
||||
)
|
||||
|
||||
# Render questions
|
||||
questions_html = []
|
||||
for q_idx, result in filtered_questions:
|
||||
is_invalid = result.get("is_invalid", False)
|
||||
is_correct = result.get("is_correct", False)
|
||||
question = result.get("question", "")
|
||||
correct_answer = result.get("correct_answer", "")
|
||||
predicted_answer = result.get("predicted_answer", "")
|
||||
category = get_category_name(result.get("category", "Unknown"))
|
||||
|
||||
icon = "⚠️" if is_invalid else ("✅" if is_correct else "❌")
|
||||
border_class = "border-l-4 border-yellow-500" if is_invalid else ("border-l-4 border-green-600" if is_correct else "border-l-4 border-red-600")
|
||||
|
||||
questions_html.append(
|
||||
Div(
|
||||
# Header
|
||||
Div(
|
||||
P(f"{icon} Question {q_idx + 1}", cls="text-lg font-semibold text-foreground"),
|
||||
P(f"Category: {category}", cls="text-sm text-muted-foreground"),
|
||||
cls="mb-4"
|
||||
),
|
||||
|
||||
# Question
|
||||
Div(
|
||||
P("Question:", cls="text-sm font-medium text-foreground mb-1"),
|
||||
P(question, cls="text-foreground"),
|
||||
cls="mb-4"
|
||||
),
|
||||
|
||||
# Answers side by side
|
||||
Div(
|
||||
Div(
|
||||
P("✓ Correct Answer", cls="text-sm font-medium text-foreground mb-2"),
|
||||
Div(correct_answer, cls="bg-green-50 border border-green-200 rounded-md p-3 text-foreground"),
|
||||
cls="flex-1"
|
||||
),
|
||||
Div(
|
||||
P(f"{'✓' if is_correct else '✗'} Predicted Answer", cls="text-sm font-medium text-foreground mb-2"),
|
||||
Div(predicted_answer, cls=f"border rounded-md p-3 text-foreground " + ("bg-green-50 border-green-200" if is_correct else "bg-red-50 border-red-200")),
|
||||
cls="flex-1"
|
||||
),
|
||||
cls="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4"
|
||||
),
|
||||
|
||||
# Details
|
||||
Details(
|
||||
Summary("📝 Show Reasoning & Retrieved Memories", cls="cursor-pointer font-medium text-foreground hover:text-primary py-2"),
|
||||
Div(
|
||||
# System Reasoning
|
||||
Div(
|
||||
P("System Reasoning:", cls="text-sm font-medium text-foreground mb-2"),
|
||||
Pre(result.get("reasoning", "N/A"), cls="bg-slate-900 text-slate-100 p-3 rounded-md overflow-x-auto text-sm"),
|
||||
cls="mb-4"
|
||||
),
|
||||
# Judge Reasoning
|
||||
Div(
|
||||
P("Judge Reasoning:", cls="text-sm font-medium text-foreground mb-2"),
|
||||
Pre(result.get("correctness_reasoning", "N/A"), cls="bg-slate-900 text-slate-100 p-3 rounded-md overflow-x-auto text-sm"),
|
||||
cls="mb-4"
|
||||
),
|
||||
# Retrieved Memories
|
||||
Div(
|
||||
P(f"Retrieved Memories ({len(result.get('retrieved_memories', []))}):", cls="text-sm font-medium text-foreground mb-2"),
|
||||
*[
|
||||
Div(
|
||||
P(f"#{i+1} • Score: {mem.get('score', 0):.4f} • Type: {mem.get('fact_type', 'N/A').upper()}", cls="text-xs text-muted-foreground mb-1"),
|
||||
P(mem.get('text', ''), cls="text-sm text-foreground"),
|
||||
cls="bg-muted/50 border border-border rounded-md p-3 mb-2"
|
||||
)
|
||||
for i, mem in enumerate(result.get("retrieved_memories", []))
|
||||
] if result.get("retrieved_memories") else [P("No memories retrieved", cls="text-sm text-muted-foreground")],
|
||||
),
|
||||
cls="mt-3 space-y-2"
|
||||
),
|
||||
cls="border border-border rounded-md p-4 bg-muted/30"
|
||||
),
|
||||
|
||||
cls=f"bg-white border border-border rounded-lg p-6 mb-4 shadow-sm {border_class}"
|
||||
)
|
||||
)
|
||||
|
||||
return (
|
||||
Title(f"{item_id}"),
|
||||
get_head(),
|
||||
Main(
|
||||
Div(
|
||||
A(f"← Back to LoComo ({mode})", href=f"/locomo/{mode}", cls="inline-flex items-center px-4 py-2 bg-white border border-border rounded-md text-sm font-medium text-foreground hover:bg-accent mb-6"),
|
||||
H3(f"📊 {item_id} - {accuracy:.2f}%", cls="text-2xl font-bold text-foreground mb-4"),
|
||||
Hr(cls="my-6 border-border"),
|
||||
q_filters,
|
||||
P(f"Showing {len(filtered_questions)} questions", cls="text-sm text-muted-foreground mb-6"),
|
||||
Div(
|
||||
*questions_html,
|
||||
cls="space-y-4"
|
||||
),
|
||||
cls="container mx-auto max-w-7xl px-4 py-8"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@rt("/longmemeval")
|
||||
def get_longmemeval(filter_type: str = "all", category_filter: str = "all"):
|
||||
"""Render LongMemEval results."""
|
||||
data = load_longmemeval_results()
|
||||
|
||||
if not data:
|
||||
return (
|
||||
Title("LongMemEval - Not Found"),
|
||||
get_head(),
|
||||
Main(
|
||||
H1("⚠️ Benchmark Results Not Found"),
|
||||
P("The benchmark results are not available."),
|
||||
H4("To generate results:"),
|
||||
Pre("./scripts/benchmarks/run-longmemeval.sh --env local"),
|
||||
A("← Back", href="/", cls="btn mt-3"),
|
||||
cls="container"
|
||||
)
|
||||
)
|
||||
|
||||
all_results = data.get("item_results", [])
|
||||
|
||||
# Filter items based on their questions
|
||||
results = []
|
||||
for item in all_results:
|
||||
detailed_results = item.get("metrics", {}).get("detailed_results", [])
|
||||
|
||||
# Apply correctness filter
|
||||
passes_correctness_filter = False
|
||||
if filter_type == "all":
|
||||
passes_correctness_filter = True
|
||||
elif filter_type == "correct":
|
||||
# Show items where all questions are correct (and not invalid)
|
||||
passes_correctness_filter = detailed_results and all(r.get("is_correct") and not r.get("is_invalid") for r in detailed_results)
|
||||
elif filter_type == "incorrect":
|
||||
# Show items that have at least one incorrect question
|
||||
passes_correctness_filter = any(not r.get("is_correct") and not r.get("is_invalid") for r in detailed_results)
|
||||
elif filter_type == "invalid":
|
||||
# Show items that have at least one invalid question
|
||||
passes_correctness_filter = any(r.get("is_invalid") for r in detailed_results)
|
||||
|
||||
# Apply category filter
|
||||
passes_category_filter = False
|
||||
if category_filter == "all":
|
||||
passes_category_filter = True
|
||||
else:
|
||||
# Show items that have at least one question of the specified category
|
||||
passes_category_filter = any(r.get("category") == category_filter for r in detailed_results)
|
||||
|
||||
if passes_correctness_filter and passes_category_filter:
|
||||
results.append(item)
|
||||
|
||||
# Calculate stats (use all_results for overall stats, not filtered results)
|
||||
category_stats = {}
|
||||
total_invalid = 0
|
||||
|
||||
for item in all_results:
|
||||
if item.get("metrics", {}).get("category_stats"):
|
||||
for category, stats in item["metrics"]["category_stats"].items():
|
||||
if category not in category_stats:
|
||||
category_stats[category] = {"name": category, "correct": 0, "total": 0, "invalid": 0}
|
||||
category_stats[category]["correct"] += stats.get("correct", 0)
|
||||
category_stats[category]["total"] += stats.get("total", 0)
|
||||
category_stats[category]["invalid"] += stats.get("invalid", 0)
|
||||
|
||||
if item.get("metrics", {}).get("detailed_results"):
|
||||
for result in item["metrics"]["detailed_results"]:
|
||||
if result.get("is_invalid"):
|
||||
total_invalid += 1
|
||||
|
||||
# Overall stats
|
||||
stats_html = Div(
|
||||
H3("LongMemEval Benchmark - Overall Performance", cls="text-2xl font-bold text-foreground mb-6"),
|
||||
Div(
|
||||
Div(
|
||||
P("Overall Accuracy", cls="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2"),
|
||||
P(f"{data['overall_accuracy']:.2f}%", cls="text-3xl font-bold text-foreground"),
|
||||
cls="bg-white border border-border rounded-lg p-6 text-center shadow-sm"
|
||||
),
|
||||
Div(
|
||||
P("Correct Answers", cls="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2"),
|
||||
P(f"{data['total_correct']} / {data['total_questions']}", cls="text-3xl font-bold text-foreground"),
|
||||
cls="bg-white border border-border rounded-lg p-6 text-center shadow-sm"
|
||||
),
|
||||
Div(
|
||||
P("Invalid Questions", cls="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2"),
|
||||
P(str(total_invalid), cls="text-3xl font-bold text-foreground"),
|
||||
cls="bg-white border border-border rounded-lg p-6 text-center shadow-sm"
|
||||
) if total_invalid > 0 else None,
|
||||
Div(
|
||||
P("Items", cls="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2"),
|
||||
P(str(len(all_results)), cls="text-3xl font-bold text-foreground"),
|
||||
cls="bg-white border border-border rounded-lg p-6 text-center shadow-sm"
|
||||
),
|
||||
cls="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8"
|
||||
),
|
||||
H4("Accuracy by Category", cls="text-xl font-semibold text-foreground mb-4"),
|
||||
Div(
|
||||
*[
|
||||
Div(
|
||||
P(cat["name"], cls="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2"),
|
||||
P(f"{(cat['correct'] / (cat['total'] - cat['invalid']) * 100) if (cat['total'] - cat['invalid']) > 0 else 0:.1f}%", cls="text-2xl font-bold text-foreground"),
|
||||
P(f"{cat['correct']} / {cat['total']}", cls="text-sm text-muted-foreground mt-1"),
|
||||
cls="bg-white border border-border rounded-lg p-6 text-center shadow-sm"
|
||||
)
|
||||
for cat in category_stats.values()
|
||||
],
|
||||
cls="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"
|
||||
) if category_stats else None
|
||||
)
|
||||
|
||||
# Filter controls
|
||||
filters = Div(
|
||||
# Correctness filter
|
||||
Div(
|
||||
P("Filter by correctness:", cls="text-sm font-medium text-foreground mb-2"),
|
||||
Div(
|
||||
A("All", href=f"/longmemeval?filter_type=all&category_filter={category_filter}",
|
||||
cls="px-3 py-1.5 rounded-md text-sm font-medium " + ("bg-primary text-primary-foreground" if filter_type == 'all' else "bg-white text-foreground border border-border hover:bg-accent")),
|
||||
A("✅ All Correct", href=f"/longmemeval?filter_type=correct&category_filter={category_filter}",
|
||||
cls="px-3 py-1.5 rounded-md text-sm font-medium " + ("bg-primary text-primary-foreground" if filter_type == 'correct' else "bg-white text-foreground border border-border hover:bg-accent")),
|
||||
A("❌ Has Incorrect", href=f"/longmemeval?filter_type=incorrect&category_filter={category_filter}",
|
||||
cls="px-3 py-1.5 rounded-md text-sm font-medium " + ("bg-primary text-primary-foreground" if filter_type == 'incorrect' else "bg-white text-foreground border border-border hover:bg-accent")),
|
||||
A("⚠️ Has Invalid", href=f"/longmemeval?filter_type=invalid&category_filter={category_filter}",
|
||||
cls="px-3 py-1.5 rounded-md text-sm font-medium " + ("bg-primary text-primary-foreground" if filter_type == 'invalid' else "bg-white text-foreground border border-border hover:bg-accent")) if total_invalid > 0 else None,
|
||||
cls="flex flex-wrap gap-2"
|
||||
),
|
||||
cls="mb-4"
|
||||
),
|
||||
# Category filter
|
||||
Div(
|
||||
P("Filter by question category:", cls="text-sm font-medium text-foreground mb-2"),
|
||||
Div(
|
||||
A("All Categories", href=f"/longmemeval?filter_type={filter_type}&category_filter=all",
|
||||
cls="px-3 py-1.5 rounded-md text-sm font-medium " + ("bg-primary text-primary-foreground" if category_filter == 'all' else "bg-white text-foreground border border-border hover:bg-accent")),
|
||||
A("Multi-session", href=f"/longmemeval?filter_type={filter_type}&category_filter=multi-session",
|
||||
cls="px-3 py-1.5 rounded-md text-sm font-medium " + ("bg-primary text-primary-foreground" if category_filter == 'multi-session' else "bg-white text-foreground border border-border hover:bg-accent")),
|
||||
A("Single-session User", href=f"/longmemeval?filter_type={filter_type}&category_filter=single-session-user",
|
||||
cls="px-3 py-1.5 rounded-md text-sm font-medium " + ("bg-primary text-primary-foreground" if category_filter == 'single-session-user' else "bg-white text-foreground border border-border hover:bg-accent")),
|
||||
A("Single-session Assistant", href=f"/longmemeval?filter_type={filter_type}&category_filter=single-session-assistant",
|
||||
cls="px-3 py-1.5 rounded-md text-sm font-medium " + ("bg-primary text-primary-foreground" if category_filter == 'single-session-assistant' else "bg-white text-foreground border border-border hover:bg-accent")),
|
||||
A("Single-session Preference", href=f"/longmemeval?filter_type={filter_type}&category_filter=single-session-preference",
|
||||
cls="px-3 py-1.5 rounded-md text-sm font-medium " + ("bg-primary text-primary-foreground" if category_filter == 'single-session-preference' else "bg-white text-foreground border border-border hover:bg-accent")),
|
||||
A("Temporal Reasoning", href=f"/longmemeval?filter_type={filter_type}&category_filter=temporal-reasoning",
|
||||
cls="px-3 py-1.5 rounded-md text-sm font-medium " + ("bg-primary text-primary-foreground" if category_filter == 'temporal-reasoning' else "bg-white text-foreground border border-border hover:bg-accent")),
|
||||
A("Knowledge Update", href=f"/longmemeval?filter_type={filter_type}&category_filter=knowledge-update",
|
||||
cls="px-3 py-1.5 rounded-md text-sm font-medium " + ("bg-primary text-primary-foreground" if category_filter == 'knowledge-update' else "bg-white text-foreground border border-border hover:bg-accent")),
|
||||
cls="flex flex-wrap gap-2"
|
||||
),
|
||||
cls="mb-4"
|
||||
),
|
||||
cls="mb-6"
|
||||
)
|
||||
|
||||
# Render items
|
||||
items_html = []
|
||||
for item in results:
|
||||
# Find the original index in all_results
|
||||
original_idx = all_results.index(item)
|
||||
item_id = item.get("item_id", f"item-{original_idx}")
|
||||
metrics = item.get("metrics", {})
|
||||
accuracy = metrics.get("accuracy", 0)
|
||||
correct = metrics.get("correct", 0)
|
||||
total = metrics.get("total", 0)
|
||||
|
||||
color = "🟢" if accuracy >= 70 else ("🟡" if accuracy >= 50 else "🔴")
|
||||
|
||||
# Determine border color based on accuracy
|
||||
if accuracy >= 70:
|
||||
border_class = "border-l-4 border-green-600"
|
||||
bg_class = "hover:bg-green-50"
|
||||
elif accuracy >= 50:
|
||||
border_class = "border-l-4 border-yellow-500"
|
||||
bg_class = "hover:bg-yellow-50"
|
||||
else:
|
||||
border_class = "border-l-4 border-red-600"
|
||||
bg_class = "hover:bg-red-50"
|
||||
|
||||
items_html.append(
|
||||
A(
|
||||
Div(
|
||||
Div(
|
||||
P(f"{color} {item_id}", cls="text-lg font-semibold text-foreground mb-2"),
|
||||
Div(
|
||||
Div(
|
||||
P("Accuracy", cls="text-xs font-medium text-muted-foreground uppercase tracking-wide"),
|
||||
P(f"{accuracy:.1f}%", cls="text-2xl font-bold text-foreground"),
|
||||
cls="text-center"
|
||||
),
|
||||
Div(
|
||||
P("Correct", cls="text-xs font-medium text-muted-foreground uppercase tracking-wide"),
|
||||
P(f"{correct}/{total}", cls="text-xl font-semibold text-foreground"),
|
||||
cls="text-center"
|
||||
),
|
||||
cls="flex gap-6 items-center"
|
||||
),
|
||||
cls="p-6"
|
||||
),
|
||||
cls=f"bg-white border border-border rounded-lg shadow-sm transition-all {border_class} {bg_class}"
|
||||
),
|
||||
href=f"/longmemeval/item/{original_idx}?filter_type={filter_type}&category_filter={category_filter}",
|
||||
cls="block no-underline"
|
||||
)
|
||||
)
|
||||
|
||||
return (
|
||||
Title("LongMemEval"),
|
||||
get_head(),
|
||||
Main(
|
||||
Div(
|
||||
A("← Back to benchmarks", href="/", cls="inline-flex items-center px-4 py-2 bg-white border border-border rounded-md text-sm font-medium text-foreground hover:bg-accent mb-6"),
|
||||
stats_html,
|
||||
Hr(cls="my-6 border-border"),
|
||||
filters,
|
||||
P(f"Showing {len(results)} of {len(all_results)} items", cls="text-sm text-muted-foreground mb-6"),
|
||||
Div(
|
||||
*items_html,
|
||||
cls="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"
|
||||
),
|
||||
cls="container mx-auto max-w-7xl px-4 py-8"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@rt("/longmemeval/item/{item_idx}")
|
||||
def get_longmemeval_item(item_idx: int, filter_type: str = "all"):
|
||||
"""Render a single LongMemEval item with questions."""
|
||||
data = load_longmemeval_results()
|
||||
if not data:
|
||||
return Redirect("/")
|
||||
|
||||
results = data.get("item_results", [])
|
||||
if item_idx >= len(results):
|
||||
return Redirect("/longmemeval")
|
||||
|
||||
item = results[item_idx]
|
||||
item_id = item.get("item_id", f"item-{item_idx}")
|
||||
metrics = item.get("metrics", {})
|
||||
accuracy = metrics.get("accuracy", 0)
|
||||
detailed_results = metrics.get("detailed_results", [])
|
||||
|
||||
# Filter questions
|
||||
filtered_questions = []
|
||||
for q_idx, result in enumerate(detailed_results):
|
||||
is_invalid = result.get("is_invalid", False)
|
||||
is_correct = result.get("is_correct", False)
|
||||
|
||||
if filter_type == "all":
|
||||
filtered_questions.append((q_idx, result))
|
||||
elif filter_type == "correct" and is_correct and not is_invalid:
|
||||
filtered_questions.append((q_idx, result))
|
||||
elif filter_type == "incorrect" and not is_correct and not is_invalid:
|
||||
filtered_questions.append((q_idx, result))
|
||||
elif filter_type == "invalid" and is_invalid:
|
||||
filtered_questions.append((q_idx, result))
|
||||
|
||||
# Filters for questions
|
||||
has_invalid = any(r.get("is_invalid", False) for r in detailed_results)
|
||||
q_filters = Div(
|
||||
P("Filter:", cls="text-sm font-medium text-foreground mb-2"),
|
||||
Div(
|
||||
A("All", href=f"/longmemeval/item/{item_idx}?filter_type=all",
|
||||
cls="px-3 py-1.5 rounded-md text-sm font-medium " + ("bg-primary text-primary-foreground" if filter_type == 'all' else "bg-white text-foreground border border-border hover:bg-accent")),
|
||||
A("✅ Correct", href=f"/longmemeval/item/{item_idx}?filter_type=correct",
|
||||
cls="px-3 py-1.5 rounded-md text-sm font-medium " + ("bg-primary text-primary-foreground" if filter_type == 'correct' else "bg-white text-foreground border border-border hover:bg-accent")),
|
||||
A("❌ Incorrect", href=f"/longmemeval/item/{item_idx}?filter_type=incorrect",
|
||||
cls="px-3 py-1.5 rounded-md text-sm font-medium " + ("bg-primary text-primary-foreground" if filter_type == 'incorrect' else "bg-white text-foreground border border-border hover:bg-accent")),
|
||||
A("⚠️ Invalid", href=f"/longmemeval/item/{item_idx}?filter_type=invalid",
|
||||
cls="px-3 py-1.5 rounded-md text-sm font-medium " + ("bg-primary text-primary-foreground" if filter_type == 'invalid' else "bg-white text-foreground border border-border hover:bg-accent")) if has_invalid else None,
|
||||
cls="flex flex-wrap gap-2"
|
||||
),
|
||||
cls="mb-6"
|
||||
)
|
||||
|
||||
# Render questions
|
||||
questions_html = []
|
||||
for q_idx, result in filtered_questions:
|
||||
is_invalid = result.get("is_invalid", False)
|
||||
is_correct = result.get("is_correct", False)
|
||||
question = result.get("question", "")
|
||||
correct_answer = result.get("correct_answer", "")
|
||||
predicted_answer = result.get("predicted_answer", "")
|
||||
category = result.get("category", "Unknown")
|
||||
|
||||
icon = "⚠️" if is_invalid else ("✅" if is_correct else "❌")
|
||||
border_class = "border-l-4 border-yellow-500" if is_invalid else ("border-l-4 border-green-600" if is_correct else "border-l-4 border-red-600")
|
||||
|
||||
questions_html.append(
|
||||
Div(
|
||||
# Header
|
||||
Div(
|
||||
P(f"{icon} Question {q_idx + 1}", cls="text-lg font-semibold text-foreground"),
|
||||
P(f"Category: {category}", cls="text-sm text-muted-foreground"),
|
||||
cls="mb-4"
|
||||
),
|
||||
|
||||
# Question
|
||||
Div(
|
||||
P("Question:", cls="text-sm font-medium text-foreground mb-1"),
|
||||
P(question, cls="text-foreground"),
|
||||
cls="mb-4"
|
||||
),
|
||||
|
||||
# Answers side by side
|
||||
Div(
|
||||
Div(
|
||||
P("✓ Correct Answer", cls="text-sm font-medium text-foreground mb-2"),
|
||||
Div(correct_answer, cls="bg-green-50 border border-green-200 rounded-md p-3 text-foreground"),
|
||||
cls="flex-1"
|
||||
),
|
||||
Div(
|
||||
P(f"{'✓' if is_correct else '✗'} Predicted Answer", cls="text-sm font-medium text-foreground mb-2"),
|
||||
Div(predicted_answer, cls=f"border rounded-md p-3 text-foreground " + ("bg-green-50 border-green-200" if is_correct else "bg-red-50 border-red-200")),
|
||||
cls="flex-1"
|
||||
),
|
||||
cls="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4"
|
||||
),
|
||||
|
||||
# Details
|
||||
Details(
|
||||
Summary("📝 Show Reasoning & Retrieved Memories", cls="cursor-pointer font-medium text-foreground hover:text-primary py-2"),
|
||||
Div(
|
||||
# System Reasoning
|
||||
Div(
|
||||
P("System Reasoning:", cls="text-sm font-medium text-foreground mb-2"),
|
||||
Pre(result.get("reasoning", "N/A"), cls="bg-slate-900 text-slate-100 p-3 rounded-md overflow-x-auto text-sm"),
|
||||
cls="mb-4"
|
||||
),
|
||||
# Judge Reasoning
|
||||
Div(
|
||||
P("Judge Reasoning:", cls="text-sm font-medium text-foreground mb-2"),
|
||||
Pre(result.get("correctness_reasoning", "N/A"), cls="bg-slate-900 text-slate-100 p-3 rounded-md overflow-x-auto text-sm"),
|
||||
cls="mb-4"
|
||||
),
|
||||
# Retrieved Memories
|
||||
Div(
|
||||
P(f"Retrieved Memories ({len(result.get('retrieved_memories', []))}):", cls="text-sm font-medium text-foreground mb-2"),
|
||||
*[
|
||||
Div(
|
||||
P(f"#{i+1} • Score: {mem.get('score', 0):.4f} • Type: {mem.get('fact_type', 'N/A').upper()}", cls="text-xs text-muted-foreground mb-1"),
|
||||
P(mem.get('text', ''), cls="text-sm text-foreground"),
|
||||
cls="bg-muted/50 border border-border rounded-md p-3 mb-2"
|
||||
)
|
||||
for i, mem in enumerate(result.get("retrieved_memories", []))
|
||||
] if result.get("retrieved_memories") else [P("No memories retrieved", cls="text-sm text-muted-foreground")],
|
||||
),
|
||||
cls="mt-3 space-y-2"
|
||||
),
|
||||
cls="border border-border rounded-md p-4 bg-muted/30"
|
||||
),
|
||||
|
||||
cls=f"bg-white border border-border rounded-lg p-6 mb-4 shadow-sm {border_class}"
|
||||
)
|
||||
)
|
||||
|
||||
return (
|
||||
Title(f"{item_id}"),
|
||||
get_head(),
|
||||
Main(
|
||||
Div(
|
||||
A("← Back to LongMemEval", href="/longmemeval", cls="inline-flex items-center px-4 py-2 bg-white border border-border rounded-md text-sm font-medium text-foreground hover:bg-accent mb-6"),
|
||||
H3(f"📊 {item_id} - {accuracy:.2f}%", cls="text-2xl font-bold text-foreground mb-4"),
|
||||
Hr(cls="my-6 border-border"),
|
||||
q_filters,
|
||||
P(f"Showing {len(filtered_questions)} questions", cls="text-sm text-muted-foreground mb-6"),
|
||||
Div(
|
||||
*questions_html,
|
||||
cls="space-y-4"
|
||||
),
|
||||
cls="container mx-auto max-w-7xl px-4 py-8"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
print("🚀 Starting Benchmark Visualizer...")
|
||||
print("📊 Server running at: http://localhost:8001")
|
||||
uvicorn.run("main:app", host="127.0.0.1", port=8001, reload=True)
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
"""Benchmark Visualizer Web Service.
|
||||
|
||||
A standalone web service for visualizing benchmark results.
|
||||
Supports LoComo and LongMemEval benchmark visualization.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.responses import HTMLResponse, FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
app = FastAPI(title="Benchmark Visualizer")
|
||||
|
||||
# Get the benchmarks directory
|
||||
BENCHMARKS_DIR = Path(__file__).parent.parent
|
||||
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def index():
|
||||
"""Serve the main benchmark visualizer page."""
|
||||
html_path = Path(__file__).parent / "static" / "index.html"
|
||||
with open(html_path) as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
@app.get("/api/locomo")
|
||||
async def get_locomo_results(mode: str = "search") -> dict[str, Any]:
|
||||
"""Get LoComo benchmark results.
|
||||
|
||||
Returns pre-computed benchmark results from the locomo directory.
|
||||
|
||||
Args:
|
||||
mode: Either "search" (default) or "think" to select which results to load
|
||||
"""
|
||||
try:
|
||||
# Determine filename based on mode
|
||||
if mode == "think":
|
||||
filename = "benchmark_results_think.json"
|
||||
else:
|
||||
filename = "benchmark_results.json"
|
||||
|
||||
results_path = BENCHMARKS_DIR / "locomo" / "results" / filename
|
||||
|
||||
if not results_path.exists():
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Benchmark results not found for mode '{mode}'. Please run the benchmark first with {'--use-think' if mode == 'think' else 'default settings'}."
|
||||
)
|
||||
|
||||
with open(results_path) as f:
|
||||
results = json.load(f)
|
||||
|
||||
return results
|
||||
except json.JSONDecodeError as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to parse benchmark results: {str(e)}"
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.get("/api/longmemeval")
|
||||
async def get_longmemeval_results() -> dict[str, Any]:
|
||||
"""Get LongMemEval benchmark results.
|
||||
|
||||
Returns pre-computed benchmark results from the longmemeval directory.
|
||||
"""
|
||||
try:
|
||||
results_path = BENCHMARKS_DIR / "longmemeval" / "results" / "benchmark_results.json"
|
||||
print(f"chcking path {results_path}")
|
||||
logging.info(f"chcking path {results_path}")
|
||||
|
||||
if not results_path.exists():
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Benchmark results not found. Please run the benchmark first."
|
||||
)
|
||||
|
||||
with open(results_path) as f:
|
||||
results = json.load(f)
|
||||
|
||||
return results
|
||||
except json.JSONDecodeError as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to parse benchmark results: {str(e)}"
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# Mount static files
|
||||
app.mount("/static", StaticFiles(directory=Path(__file__).parent / "static"), name="static")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
uvicorn.run(app, host="127.0.0.1", port=8001)
|
||||
|
|
@ -1,140 +0,0 @@
|
|||
body {
|
||||
font-family: Tahoma, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: #333;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-bottom: 3px solid #42a5f5;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
margin: 0 0 5px 0;
|
||||
}
|
||||
|
||||
.header p {
|
||||
margin: 0;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.benchmark-selector {
|
||||
background: #f0f0f0;
|
||||
padding: 15px 20px;
|
||||
border-bottom: 2px solid #333;
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.benchmark-selector label {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.benchmark-selector select {
|
||||
padding: 8px 12px;
|
||||
border: 2px solid #42a5f5;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
#benchmark-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.welcome-message {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.welcome-message h2 {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.load-button {
|
||||
padding: 8px 20px;
|
||||
background: #66bb6a;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.load-button:hover {
|
||||
background: #43a047;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #d32f2f;
|
||||
padding: 20px;
|
||||
background: #ffebee;
|
||||
border: 2px solid #ef5350;
|
||||
border-radius: 8px;
|
||||
margin: 20px;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.error-message h3 {
|
||||
margin-top: 0;
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
.error-message pre {
|
||||
background: #f5f5f5;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
color: #333;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
padding: 12px;
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-weight: bold;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.qa-results {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.qa-item {
|
||||
margin-bottom: 15px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Benchmark Visualizer</title>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="/static/css/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>Benchmark Visualizer</h1>
|
||||
<p>Analyze and visualize benchmark results</p>
|
||||
</div>
|
||||
|
||||
<div class="benchmark-selector">
|
||||
<label>Select Benchmark:</label>
|
||||
<select id="benchmark-select" onchange="selectBenchmark()">
|
||||
<option value="">-- Select a benchmark --</option>
|
||||
<option value="locomo-search">LoComo (search)</option>
|
||||
<option value="locomo-think">LoComo (think)</option>
|
||||
<option value="longmemeval">LongMemEval</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="benchmark-content">
|
||||
<div class="welcome-message">
|
||||
<h2>Welcome to Benchmark Visualizer</h2>
|
||||
<p>Select a benchmark from the dropdown above to view results.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,629 +0,0 @@
|
|||
// Benchmark Visualizer App
|
||||
|
||||
let currentBenchmark = null;
|
||||
let benchmarkData = null;
|
||||
|
||||
function selectBenchmark() {
|
||||
const select = document.getElementById('benchmark-select');
|
||||
currentBenchmark = select.value;
|
||||
|
||||
if (!currentBenchmark) {
|
||||
document.getElementById('benchmark-content').innerHTML = `
|
||||
<div class="welcome-message">
|
||||
<h2>Welcome to Benchmark Visualizer</h2>
|
||||
<p>Select a benchmark from the dropdown above to view results.</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Load the selected benchmark
|
||||
if (currentBenchmark === 'locomo-search') {
|
||||
loadLocomoResults('search');
|
||||
} else if (currentBenchmark === 'locomo-think') {
|
||||
loadLocomoResults('think');
|
||||
} else if (currentBenchmark === 'longmemeval') {
|
||||
loadLongMemEvalResults();
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLocomoResults(mode = 'search') {
|
||||
try {
|
||||
const response = await fetch(`/api/locomo?mode=${mode}`);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const modeLabel = mode === 'think' ? 'think' : 'search';
|
||||
const runCommand = mode === 'think'
|
||||
? 'uv run python locomo_benchmark.py --use-think'
|
||||
: 'uv run python locomo_benchmark.py';
|
||||
|
||||
document.getElementById('benchmark-content').innerHTML = `
|
||||
<div class="error-message">
|
||||
<h3>⚠️ Benchmark Results Not Found</h3>
|
||||
<p>${errorData.detail || 'The requested benchmark results are not available.'}</p>
|
||||
<p><strong>To generate ${modeLabel} mode results:</strong></p>
|
||||
<pre style="background: #f5f5f5; padding: 10px; border-radius: 4px; overflow-x: auto;">cd benchmarks/locomo
|
||||
${runCommand}</pre>
|
||||
<p style="margin-top: 15px; font-size: 14px; color: #666;">
|
||||
Once the benchmark completes, refresh this page and select "${mode === 'think' ? 'LoComo (think)' : 'LoComo (search)'}" again.
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
benchmarkData = await response.json();
|
||||
console.log(`Loaded locomo data (${mode} mode):`, benchmarkData);
|
||||
renderLocomoResults(mode);
|
||||
} catch (e) {
|
||||
console.error('Error loading benchmark results:', e);
|
||||
document.getElementById('benchmark-content').innerHTML = `
|
||||
<div class="error-message">
|
||||
<h3>❌ Error Loading Results</h3>
|
||||
<p>${e.message}</p>
|
||||
<p style="font-size: 12px; color: #666; margin-top: 10px;">Check the browser console for more details.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderLocomoResults(mode = 'search') {
|
||||
if (!benchmarkData) return;
|
||||
|
||||
const content = document.getElementById('benchmark-content');
|
||||
|
||||
try {
|
||||
// Handle both old and new structure
|
||||
const results = benchmarkData.item_results || benchmarkData.conversation_results || [];
|
||||
const numItems = benchmarkData.num_items || results.length;
|
||||
|
||||
console.log('Rendering results:', { resultsCount: results.length, numItems });
|
||||
|
||||
// Calculate per-category statistics
|
||||
const categoryStats = {
|
||||
1: { name: 'Multi-hop', correct: 0, total: 0 },
|
||||
2: { name: 'Single-hop', correct: 0, total: 0 },
|
||||
3: { name: 'Temporal', correct: 0, total: 0 },
|
||||
4: { name: 'Open-domain', correct: 0, total: 0 }
|
||||
};
|
||||
|
||||
// Aggregate across all items
|
||||
let totalInvalid = 0;
|
||||
results.forEach(item => {
|
||||
if (item.metrics && item.metrics.detailed_results) {
|
||||
item.metrics.detailed_results.forEach(result => {
|
||||
const category = result.category;
|
||||
if (categoryStats[category]) {
|
||||
categoryStats[category].total++;
|
||||
if (result.is_invalid) {
|
||||
if (!categoryStats[category].invalid) categoryStats[category].invalid = 0;
|
||||
categoryStats[category].invalid++;
|
||||
totalInvalid++;
|
||||
} else if (result.is_correct) {
|
||||
categoryStats[category].correct++;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Determine title based on mode
|
||||
const modeLabel = mode === 'think' ? ' (Think Mode)' : ' (Search Mode)';
|
||||
|
||||
// Overall stats
|
||||
const totalInvalidDisplay = totalInvalid > 0
|
||||
? `<div class="stat-item">
|
||||
<div class="stat-label">Invalid Questions</div>
|
||||
<div class="stat-value" style="color: #ff9800;">${totalInvalid}</div>
|
||||
</div>`
|
||||
: '';
|
||||
|
||||
const overallHtml = `
|
||||
<div style="background: #f9f9f9; padding: 20px; border: 2px solid #333; border-radius: 8px; margin-bottom: 20px;">
|
||||
<h3 style="margin-top: 0;">LoComo Benchmark${modeLabel} - Overall Performance</h3>
|
||||
${totalInvalid > 0 ? `<div style="background: #fff3cd; border: 1px solid #ffc107; padding: 10px; border-radius: 4px; margin-bottom: 15px;">
|
||||
<strong>⚠️ Note:</strong> ${totalInvalid} question(s) marked as invalid due to errors (excluded from accuracy calculation)
|
||||
</div>` : ''}
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">Overall Accuracy</div>
|
||||
<div class="stat-value">${benchmarkData.overall_accuracy.toFixed(2)}%</div>
|
||||
${totalInvalid > 0 ? `<div style="font-size: 11px; color: #666; margin-top: 4px;">(${benchmarkData.total_correct} / ${benchmarkData.total_valid || (benchmarkData.total_questions - totalInvalid)})</div>` : ''}
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">Correct Answers</div>
|
||||
<div class="stat-value">${benchmarkData.total_correct} / ${benchmarkData.total_questions}</div>
|
||||
</div>
|
||||
${totalInvalidDisplay}
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">Items</div>
|
||||
<div class="stat-value">${numItems}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 style="margin: 20px 0 10px 0; padding-top: 15px; border-top: 1px solid #ddd;">Accuracy by Category</h4>
|
||||
<div class="stats-grid" style="grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));">
|
||||
${Object.values(categoryStats).map(cat => {
|
||||
const invalidCount = cat.invalid || 0;
|
||||
const validTotal = cat.total - invalidCount;
|
||||
const accuracy = validTotal > 0 ? ((cat.correct / validTotal) * 100).toFixed(1) : 0;
|
||||
const color = accuracy >= 70 ? '#43a047' : accuracy >= 50 ? '#ff9800' : '#e53935';
|
||||
const invalidNote = invalidCount > 0 ? ` <span style="color: #ff9800; font-size: 10px;">(${invalidCount} invalid)</span>` : '';
|
||||
return `
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">${cat.name}</div>
|
||||
<div class="stat-value" style="color: ${color};">${accuracy}%</div>
|
||||
<div style="font-size: 11px; color: #666; margin-top: 4px;">${cat.correct} / ${cat.total}${invalidNote}</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('')}
|
||||
</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>
|
||||
${totalInvalid > 0 ? '<label><input type="radio" name="answer-filter" value="invalid" onchange="filterAnswers()"> ⚠️ Invalid Only</label>' : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Build item sections
|
||||
let itemsHtml = '';
|
||||
results.forEach((item, idx) => {
|
||||
const itemId = item.item_id || item.sample_id || `item-${idx}`;
|
||||
const accuracy = item.metrics.accuracy.toFixed(2);
|
||||
const correctCount = item.metrics.correct;
|
||||
const totalCount = item.metrics.total;
|
||||
|
||||
itemsHtml += `
|
||||
<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>📊 ${itemId}</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(item)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
content.innerHTML = overallHtml + filterHtml + itemsHtml;
|
||||
} catch (e) {
|
||||
console.error('Error rendering Locomo results:', e);
|
||||
content.innerHTML = `
|
||||
<div class="error-message">
|
||||
<strong>Error rendering results:</strong> ${e.message}<br>
|
||||
<pre style="margin-top: 10px; font-size: 11px; overflow: auto;">${e.stack}</pre>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderConversationDetails(conv) {
|
||||
if (!conv || !conv.metrics) {
|
||||
return '<div style="padding: 20px; color: #666;">No metrics available</div>';
|
||||
}
|
||||
|
||||
const results = conv.metrics.detailed_results;
|
||||
if (!results || !Array.isArray(results) || results.length === 0) {
|
||||
return '<div style="padding: 20px; color: #666;">No detailed results available</div>';
|
||||
}
|
||||
|
||||
let html = '<div class="qa-results">';
|
||||
|
||||
results.forEach((result, idx) => {
|
||||
const isInvalid = result.is_invalid || false;
|
||||
const isCorrect = result.is_correct;
|
||||
const bgColor = isInvalid ? '#fff3cd' : (isCorrect ? '#e8f5e9' : '#ffebee');
|
||||
const icon = isInvalid ? '⚠️' : (isCorrect ? '✅' : '❌');
|
||||
const category = getCategoryName(result.category);
|
||||
|
||||
html += `
|
||||
<div class="qa-item" data-correct="${isCorrect}" data-invalid="${isInvalid}" 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} ${isInvalid ? '<span style="font-size: 12px; background: #ff9800; color: white; padding: 2px 8px; border-radius: 4px; margin-left: 8px;">INVALID</span>' : ''} <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;" ${isInvalid ? 'open' : ''}>
|
||||
<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;">
|
||||
${isInvalid ? `<div style="margin-bottom: 10px; padding: 10px; background: #ffebee; border-left: 4px solid #e53935; border-radius: 4px;">
|
||||
<b style="color: #c62828;">⚠️ Error:</b>
|
||||
<div style="margin-top: 4px; color: #333;">${result.error || 'Question marked as invalid'}</div>
|
||||
</div>` : ''}
|
||||
<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 || !Array.isArray(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) => {
|
||||
if (!mem) return;
|
||||
const eventDate = mem.event_date ? new Date(mem.event_date).toLocaleString() : 'N/A';
|
||||
|
||||
// Determine border color based on fact type
|
||||
let borderColor = '#42a5f5'; // default blue
|
||||
let factTypeLabel = '';
|
||||
if (mem.fact_type) {
|
||||
factTypeLabel = `<span style="background: #666; color: white; padding: 2px 6px; border-radius: 3px; font-size: 10px; margin-left: 8px;">${mem.fact_type.toUpperCase()}</span>`;
|
||||
if (mem.fact_type === 'world') {
|
||||
borderColor = '#4caf50'; // green
|
||||
} else if (mem.fact_type === 'agent') {
|
||||
borderColor = '#ff9800'; // orange
|
||||
} else if (mem.fact_type === 'opinion') {
|
||||
borderColor = '#9c27b0'; // purple
|
||||
}
|
||||
}
|
||||
|
||||
html += `
|
||||
<div style="padding: 8px; background: #f5f5f5; border-left: 3px solid ${borderColor}; 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'} | Event Date: ${eventDate}${factTypeLabel}
|
||||
</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';
|
||||
const isInvalid = item.dataset.invalid === 'true';
|
||||
|
||||
if (filter === 'all') {
|
||||
item.style.display = 'block';
|
||||
} else if (filter === 'correct' && isCorrect && !isInvalid) {
|
||||
item.style.display = 'block';
|
||||
} else if (filter === 'incorrect' && !isCorrect && !isInvalid) {
|
||||
item.style.display = 'block';
|
||||
} else if (filter === 'invalid' && isInvalid) {
|
||||
item.style.display = 'block';
|
||||
} else {
|
||||
item.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// LongMemEval functions
|
||||
async function loadLongMemEvalResults() {
|
||||
try {
|
||||
const response = await fetch('/api/longmemeval');
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
document.getElementById('benchmark-content').innerHTML = `
|
||||
<div class="error-message">
|
||||
<h3>⚠️ Benchmark Results Not Found</h3>
|
||||
<p>${errorData.detail || 'The requested benchmark results are not available.'}</p>
|
||||
<p><strong>To generate results:</strong></p>
|
||||
<pre style="background: #f5f5f5; padding: 10px; border-radius: 4px; overflow-x: auto;">cd benchmarks/longmemeval
|
||||
uv run python longmemeval_benchmark.py</pre>
|
||||
<p style="margin-top: 15px; font-size: 14px; color: #666;">
|
||||
Once the benchmark completes, refresh this page and select "LongMemEval" again.
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
benchmarkData = await response.json();
|
||||
console.log('Loaded longmemeval data:', benchmarkData);
|
||||
renderLongMemEvalResults();
|
||||
} catch (e) {
|
||||
console.error('Error loading longmemeval results:', e);
|
||||
document.getElementById('benchmark-content').innerHTML = `
|
||||
<div class="error-message">
|
||||
<h3>❌ Error Loading Results</h3>
|
||||
<p>${e.message}</p>
|
||||
<p style="font-size: 12px; color: #666; margin-top: 10px;">Check the browser console for more details.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderLongMemEvalResults() {
|
||||
if (!benchmarkData) return;
|
||||
|
||||
const content = document.getElementById('benchmark-content');
|
||||
|
||||
try {
|
||||
const results = benchmarkData.item_results || [];
|
||||
const numItems = benchmarkData.num_items || results.length;
|
||||
|
||||
console.log('Rendering longmemeval results:', { resultsCount: results.length, numItems });
|
||||
|
||||
// Calculate per-category statistics
|
||||
const categoryStats = {};
|
||||
|
||||
// Aggregate across all items
|
||||
let totalInvalid = 0;
|
||||
results.forEach(item => {
|
||||
if (item.metrics && item.metrics.category_stats) {
|
||||
Object.entries(item.metrics.category_stats).forEach(([category, stats]) => {
|
||||
if (!categoryStats[category]) {
|
||||
categoryStats[category] = { name: category, correct: 0, total: 0, invalid: 0 };
|
||||
}
|
||||
categoryStats[category].correct += stats.correct || 0;
|
||||
categoryStats[category].total += stats.total || 0;
|
||||
categoryStats[category].invalid += stats.invalid || 0;
|
||||
});
|
||||
}
|
||||
if (item.metrics && item.metrics.detailed_results) {
|
||||
item.metrics.detailed_results.forEach(result => {
|
||||
if (result.is_invalid) {
|
||||
totalInvalid++;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Overall stats
|
||||
const totalInvalidDisplay = totalInvalid > 0
|
||||
? `<div class="stat-item">
|
||||
<div class="stat-label">Invalid Questions</div>
|
||||
<div class="stat-value" style="color: #ff9800;">${totalInvalid}</div>
|
||||
</div>`
|
||||
: '';
|
||||
|
||||
const overallHtml = `
|
||||
<div style="background: #f9f9f9; padding: 20px; border: 2px solid #333; border-radius: 8px; margin-bottom: 20px;">
|
||||
<h3 style="margin-top: 0;">LongMemEval Benchmark - Overall Performance</h3>
|
||||
${totalInvalid > 0 ? `<div style="background: #fff3cd; border: 1px solid #ffc107; padding: 10px; border-radius: 4px; margin-bottom: 15px;">
|
||||
<strong>⚠️ Note:</strong> ${totalInvalid} question(s) marked as invalid due to errors (excluded from accuracy calculation)
|
||||
</div>` : ''}
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">Overall Accuracy</div>
|
||||
<div class="stat-value">${benchmarkData.overall_accuracy.toFixed(2)}%</div>
|
||||
${totalInvalid > 0 ? `<div style="font-size: 11px; color: #666; margin-top: 4px;">(${benchmarkData.total_correct} / ${benchmarkData.total_valid || (benchmarkData.total_questions - totalInvalid)})</div>` : ''}
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">Correct Answers</div>
|
||||
<div class="stat-value">${benchmarkData.total_correct} / ${benchmarkData.total_questions}</div>
|
||||
</div>
|
||||
${totalInvalidDisplay}
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">Items</div>
|
||||
<div class="stat-value">${numItems}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 style="margin: 20px 0 10px 0; padding-top: 15px; border-top: 1px solid #ddd;">Accuracy by Category</h4>
|
||||
<div class="stats-grid" style="grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));">
|
||||
${Object.values(categoryStats).map(cat => {
|
||||
const invalidCount = cat.invalid || 0;
|
||||
const validTotal = cat.total - invalidCount;
|
||||
const accuracy = validTotal > 0 ? ((cat.correct / validTotal) * 100).toFixed(1) : 0;
|
||||
const color = accuracy >= 70 ? '#43a047' : accuracy >= 50 ? '#ff9800' : '#e53935';
|
||||
const invalidNote = invalidCount > 0 ? ` <span style="color: #ff9800; font-size: 10px;">(${invalidCount} invalid)</span>` : '';
|
||||
return `
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">${cat.name}</div>
|
||||
<div class="stat-value" style="color: ${color};">${accuracy}%</div>
|
||||
<div style="font-size: 11px; color: #666; margin-top: 4px;">${cat.correct} / ${cat.total}${invalidNote}</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('')}
|
||||
</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>
|
||||
${totalInvalid > 0 ? '<label><input type="radio" name="answer-filter" value="invalid" onchange="filterAnswers()"> ⚠️ Invalid Only</label>' : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Build item sections
|
||||
let itemsHtml = '';
|
||||
results.forEach((item, idx) => {
|
||||
const itemId = item.item_id || `item-${idx}`;
|
||||
const accuracy = item.metrics.accuracy.toFixed(2);
|
||||
const correctCount = item.metrics.correct;
|
||||
const totalCount = item.metrics.total;
|
||||
|
||||
itemsHtml += `
|
||||
<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>📊 ${itemId}</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;">
|
||||
${renderLongMemEvalItemDetails(item)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
content.innerHTML = overallHtml + filterHtml + itemsHtml;
|
||||
} catch (e) {
|
||||
console.error('Error rendering LongMemEval results:', e);
|
||||
content.innerHTML = `
|
||||
<div class="error-message">
|
||||
<strong>Error rendering results:</strong> ${e.message}<br>
|
||||
<pre style="margin-top: 10px; font-size: 11px; overflow: auto;">${e.stack}</pre>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderLongMemEvalItemDetails(item) {
|
||||
if (!item || !item.metrics) {
|
||||
return '<div style="padding: 20px; color: #666;">No metrics available</div>';
|
||||
}
|
||||
|
||||
const results = item.metrics.detailed_results;
|
||||
if (!results || !Array.isArray(results) || results.length === 0) {
|
||||
return '<div style="padding: 20px; color: #666;">No detailed results available</div>';
|
||||
}
|
||||
|
||||
let html = '<div class="qa-results">';
|
||||
|
||||
results.forEach((result, idx) => {
|
||||
const isInvalid = result.is_invalid || false;
|
||||
const isCorrect = result.is_correct;
|
||||
const bgColor = isInvalid ? '#fff3cd' : (isCorrect ? '#e8f5e9' : '#ffebee');
|
||||
const icon = isInvalid ? '⚠️' : (isCorrect ? '✅' : '❌');
|
||||
const category = result.category || 'Unknown';
|
||||
|
||||
html += `
|
||||
<div class="qa-item" data-correct="${isCorrect}" data-invalid="${isInvalid}" 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} ${isInvalid ? '<span style="font-size: 12px; background: #ff9800; color: white; padding: 2px 8px; border-radius: 4px; margin-left: 8px;">INVALID</span>' : ''} <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;" ${isInvalid ? 'open' : ''}>
|
||||
<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;">
|
||||
${isInvalid ? `<div style="margin-bottom: 10px; padding: 10px; background: #ffebee; border-left: 4px solid #e53935; border-radius: 4px;">
|
||||
<b style="color: #c62828;">⚠️ Error:</b>
|
||||
<div style="margin-top: 4px; color: #333;">${result.error || 'Question marked as invalid'}</div>
|
||||
</div>` : ''}
|
||||
<div style="margin-bottom: 10px;">
|
||||
<b>System Reasoning:</b>
|
||||
<div style="padding: 8px; background: #f5f5f5; border-radius: 4px; margin-top: 4px;">
|
||||
${result.reasoning || 'N/A'}
|
||||
</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;
|
||||
}
|
||||
3
control-plane/.env.local.example
Normal file
3
control-plane/.env.local.example
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Dataplane API Configuration
|
||||
# URL of the Python FastAPI dataplane server
|
||||
NEXT_PUBLIC_DATAPLANE_API_URL=http://localhost:8080
|
||||
3
control-plane/.eslintrc.json
Normal file
3
control-plane/.eslintrc.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
36
control-plane/.gitignore
vendored
Normal file
36
control-plane/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
.env
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
247
control-plane/README.md
Normal file
247
control-plane/README.md
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
# Memory Control Plane
|
||||
|
||||
Control plane UI for the temporal semantic memory system built with Next.js, React, TypeScript, Tailwind CSS, and Cytoscape.js.
|
||||
|
||||
## Overview
|
||||
|
||||
The control plane is a modern web application that provides a comprehensive UI for managing and visualizing temporal semantic memories. It acts as a proxy between the browser and the Python FastAPI dataplane, eliminating CORS issues and providing a clean separation of concerns.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Browser ←→ Control Plane (Next.js) ←→ Dataplane (Python FastAPI)
|
||||
```
|
||||
|
||||
The control plane:
|
||||
- Serves the React UI to the browser
|
||||
- Provides Next.js API routes (`/api/*`) that proxy requests to the dataplane
|
||||
- Handles client-side state management and visualization
|
||||
- Eliminates CORS issues by serving both UI and API from the same origin
|
||||
|
||||
## Features
|
||||
|
||||
### 🔍 Search Debug (Most Important)
|
||||
- **Multi-pane search interface**: Add multiple search panes for comparison
|
||||
- **Interactive search controls**: Query, fact type, thinking budget, reranker selection, max tokens
|
||||
- **Phase-based visualization**: Four phases of the retrieval pipeline
|
||||
- **1. Retrieval**: View results from each method (Semantic, BM25, Graph, Temporal) with ranks and scores
|
||||
- **2. RRF Merge**: See how Reciprocal Rank Fusion combines rankings from different methods
|
||||
- **3. Reranking**: Compare before/after reranking with rank changes highlighted (blue = improved)
|
||||
- **4. Final Results**: Detailed score breakdown with activation, similarity, recency, frequency ranks
|
||||
- **Comprehensive stats**: Nodes visited, entry points, budget usage, results count, duration
|
||||
- **Trace visualization**: See exactly how each retrieval method performs and contributes
|
||||
|
||||
### 📊 Data Visualization
|
||||
- **World Facts**: View and explore general knowledge memories
|
||||
- **Agent Facts**: Track agent actions and activities
|
||||
- **Opinions**: Monitor agent beliefs and perspectives
|
||||
- **Documents**: Manage source documents
|
||||
|
||||
Each fact type supports:
|
||||
- Interactive graph visualization with Cytoscape.js (circle, grid, force-directed layouts)
|
||||
- Searchable table view with filtering
|
||||
- Real-time data loading
|
||||
|
||||
### 💭 Think Interface
|
||||
- Ask questions to the AI agent
|
||||
- View source facts used (world, agent, opinions)
|
||||
- See newly formed opinions with confidence scores
|
||||
- Configurable thinking budget
|
||||
|
||||
### ➕ Add Memory
|
||||
- Submit new memories with context
|
||||
- Support for event dates and document metadata
|
||||
- Sync or async processing options
|
||||
- Upsert capability for updates
|
||||
|
||||
### 📈 Statistics & Operations
|
||||
- Real-time memory statistics (nodes, links, documents)
|
||||
- Breakdown by fact type and link type
|
||||
- Async operation monitoring (pending/failed)
|
||||
- Auto-refresh every 5 seconds
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 18.x or later
|
||||
- A running dataplane API server (Python FastAPI)
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
Configure the dataplane URL in `.env.local`:
|
||||
|
||||
```bash
|
||||
cp .env.local.example .env.local
|
||||
```
|
||||
|
||||
Edit `.env.local`:
|
||||
|
||||
```env
|
||||
NEXT_PUBLIC_DATAPLANE_API_URL=http://localhost:8080
|
||||
```
|
||||
|
||||
### Development
|
||||
|
||||
**Terminal 1 - Start Dataplane:**
|
||||
```bash
|
||||
# From project root
|
||||
./scripts/start-server.sh
|
||||
```
|
||||
|
||||
**Terminal 2 - Start Control Plane:**
|
||||
```bash
|
||||
cd control-plane
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) in your browser.
|
||||
|
||||
### Production Build
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
npm start
|
||||
```
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Framework**: Next.js 16 with App Router
|
||||
- **Language**: TypeScript
|
||||
- **Styling**: Tailwind CSS v4
|
||||
- **Visualization**: Cytoscape.js
|
||||
- **State Management**: React Context API
|
||||
- **API**: Next.js API Routes (proxy to dataplane)
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
control-plane/
|
||||
├── src/
|
||||
│ ├── app/
|
||||
│ │ ├── api/ # API routes (proxy to dataplane)
|
||||
│ │ │ ├── agents/ # GET /api/agents
|
||||
│ │ │ ├── graph/ # GET /api/graph
|
||||
│ │ │ ├── list/ # GET /api/list
|
||||
│ │ │ ├── search/ # POST /api/search
|
||||
│ │ │ ├── think/ # POST /api/think
|
||||
│ │ │ ├── memories/
|
||||
│ │ │ │ ├── batch/ # POST /api/memories/batch
|
||||
│ │ │ │ └── batch_async/ # POST /api/memories/batch_async
|
||||
│ │ │ ├── documents/
|
||||
│ │ │ │ ├── route.ts # GET /api/documents
|
||||
│ │ │ │ └── [documentId]/ # GET /api/documents/:id
|
||||
│ │ │ ├── stats/
|
||||
│ │ │ │ └── [agentId]/ # GET /api/stats/:id
|
||||
│ │ │ └── operations/
|
||||
│ │ │ └── [agentId]/ # GET /api/operations/:id
|
||||
│ │ ├── dashboard/
|
||||
│ │ │ └── page.tsx # Main dashboard
|
||||
│ │ ├── layout.tsx # Root layout
|
||||
│ │ ├── page.tsx # Home (redirects to dashboard)
|
||||
│ │ └── globals.css # Global styles
|
||||
│ ├── components/
|
||||
│ │ ├── agent-selector.tsx # Agent dropdown
|
||||
│ │ ├── data-view.tsx # Graph/table visualization
|
||||
│ │ ├── documents-view.tsx # Document management
|
||||
│ │ ├── think-view.tsx # AI thinking interface
|
||||
│ │ ├── add-memory-view.tsx # Memory submission form
|
||||
│ │ └── stats-view.tsx # Statistics dashboard
|
||||
│ └── lib/
|
||||
│ ├── agent-context.tsx # Global agent state
|
||||
│ ├── api.ts # API client
|
||||
│ └── utils.ts # Utilities
|
||||
├── .env.local # Environment config
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## API Routes
|
||||
|
||||
All control plane API routes proxy to the dataplane:
|
||||
|
||||
| Route | Method | Description |
|
||||
|-------|--------|-------------|
|
||||
| `/api/agents` | GET | List all agents |
|
||||
| `/api/graph` | GET | Get graph data for visualization |
|
||||
| `/api/list` | GET | List memory units with search |
|
||||
| `/api/search` | POST | Search memories |
|
||||
| `/api/think` | POST | Generate AI answers |
|
||||
| `/api/memories/batch` | POST | Store memories (sync) |
|
||||
| `/api/memories/batch_async` | POST | Store memories (async) |
|
||||
| `/api/documents` | GET | List documents |
|
||||
| `/api/documents/:id` | GET | Get document details |
|
||||
| `/api/stats/:agentId` | GET | Get agent statistics |
|
||||
| `/api/operations/:agentId` | GET | List async operations |
|
||||
|
||||
## Usage
|
||||
|
||||
### Using Search Debug (Primary Feature)
|
||||
1. Go to the **Search Debug** tab
|
||||
2. Enter a search query
|
||||
3. Select fact type (World, Agent, Opinion)
|
||||
4. Adjust thinking budget, reranker (Heuristic/Cross-Encoder), and max tokens
|
||||
5. Click **Search** to run the query
|
||||
6. Use the phase radio buttons to explore the retrieval pipeline:
|
||||
- **1. Retrieval**: Switch between Semantic/BM25/Graph/Temporal tabs to see each method's results
|
||||
- **2. RRF Merge**: View how rankings from different methods are combined with source ranks
|
||||
- **3. Reranking**: See rank changes (↑ improved, ↓ declined) with score component breakdowns
|
||||
- **4. Final Results**: Detailed table with all score components and individual metric ranks
|
||||
7. Monitor the status bar showing nodes visited, entry points, budget usage, and duration
|
||||
8. Add more panes with **+ Add Search Pane** to compare different queries side-by-side
|
||||
9. Each pane maintains independent state for query, settings, and current phase view
|
||||
|
||||
### Selecting an Agent
|
||||
1. Use the dropdown in the top navigation bar
|
||||
2. Select an agent to view their memories
|
||||
3. All views will automatically filter by the selected agent
|
||||
|
||||
### Visualizing Memories
|
||||
1. Go to the **Data** tab
|
||||
2. Choose a fact type (World, Agent, Opinions, or Documents)
|
||||
3. Click **Load** to fetch data
|
||||
4. Toggle between **Graph** and **Table** views
|
||||
5. Use search to filter results
|
||||
|
||||
### Asking Questions
|
||||
1. Go to the **Think** tab
|
||||
2. Enter your question
|
||||
3. Adjust thinking budget if needed
|
||||
4. Click **Think** to get an AI-generated answer
|
||||
5. View source facts and new opinions formed
|
||||
|
||||
### Adding Memories
|
||||
1. Go to the **Add Memory** tab
|
||||
2. Enter memory content (required)
|
||||
3. Optionally add context, date, document metadata
|
||||
4. Choose sync or async processing
|
||||
5. Click **Submit Memory**
|
||||
|
||||
### Monitoring Stats
|
||||
1. Go to the **Stats & Operations** tab
|
||||
2. View real-time statistics
|
||||
3. Monitor pending/failed async operations
|
||||
4. Stats auto-refresh every 5 seconds
|
||||
|
||||
## Development Notes
|
||||
|
||||
- The control plane uses client-side rendering for interactive features
|
||||
- API routes run on the server and proxy to the dataplane
|
||||
- No direct browser-to-dataplane communication (no CORS issues)
|
||||
- Graph visualization uses Cytoscape.js with multiple layout options
|
||||
- Tailwind CSS v4 for styling (simplified configuration)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**CORS Errors**: The control plane should eliminate CORS issues. If you see them, ensure you're accessing the control plane at `http://localhost:3000` (not the dataplane directly).
|
||||
|
||||
**Connection Errors**: Verify the dataplane is running at the URL specified in `.env.local` (default: `http://localhost:8080`).
|
||||
|
||||
**Graph Not Rendering**: Check browser console for errors. Ensure data is loading correctly from `/api/graph`.
|
||||
|
||||
**Build Warnings**: The "workspace root" warning about lockfiles is harmless and can be ignored.
|
||||
20
control-plane/components.json
Normal file
20
control-plane/components.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "src/app/globals.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
}
|
||||
}
|
||||
7
control-plane/next.config.ts
Normal file
7
control-plane/next.config.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
6464
control-plane/package-lock.json
generated
Normal file
6464
control-plane/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
37
control-plane/package.json
Normal file
37
control-plane/package.json
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"name": "control-plane",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "Control plane for the temporal semantic memory system",
|
||||
"dependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.17",
|
||||
"@types/node": "^24.10.0",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cytoscape": "^3.33.1",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-config-next": "^16.0.1",
|
||||
"lucide-react": "^0.553.0",
|
||||
"next": "^16.0.1",
|
||||
"postcss": "^8.5.6",
|
||||
"react": "^19.2.0",
|
||||
"react-cytoscape": "^1.0.6",
|
||||
"react-dom": "^19.2.0",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
8
control-plane/postcss.config.mjs
Normal file
8
control-plane/postcss.config.mjs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
17
control-plane/src/app/api/agents/route.ts
Normal file
17
control-plane/src/app/api/agents/route.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
|
||||
const DATAPLANE_URL = process.env.NEXT_PUBLIC_DATAPLANE_API_URL || 'http://localhost:8080';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const response = await fetch(`${DATAPLANE_URL}/api/agents`);
|
||||
const data = await response.json();
|
||||
return NextResponse.json(data, { status: response.status });
|
||||
} catch (error) {
|
||||
console.error('Error fetching agents:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch agents' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
26
control-plane/src/app/api/documents/[documentId]/route.ts
Normal file
26
control-plane/src/app/api/documents/[documentId]/route.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const DATAPLANE_URL = process.env.NEXT_PUBLIC_DATAPLANE_API_URL || 'http://localhost:8080';
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ documentId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { documentId } = await params;
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const response = await fetch(
|
||||
`${DATAPLANE_URL}/api/documents/${documentId}?${queryString}`
|
||||
);
|
||||
const data = await response.json();
|
||||
return NextResponse.json(data, { status: response.status });
|
||||
} catch (error) {
|
||||
console.error('Error fetching document:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch document' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
20
control-plane/src/app/api/documents/route.ts
Normal file
20
control-plane/src/app/api/documents/route.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const DATAPLANE_URL = process.env.NEXT_PUBLIC_DATAPLANE_API_URL || 'http://localhost:8080';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const response = await fetch(`${DATAPLANE_URL}/api/documents?${queryString}`);
|
||||
const data = await response.json();
|
||||
return NextResponse.json(data, { status: response.status });
|
||||
} catch (error) {
|
||||
console.error('Error fetching documents:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch documents' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
20
control-plane/src/app/api/graph/route.ts
Normal file
20
control-plane/src/app/api/graph/route.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const DATAPLANE_URL = process.env.NEXT_PUBLIC_DATAPLANE_API_URL || 'http://localhost:8080';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const response = await fetch(`${DATAPLANE_URL}/api/graph?${queryString}`);
|
||||
const data = await response.json();
|
||||
return NextResponse.json(data, { status: response.status });
|
||||
} catch (error) {
|
||||
console.error('Error fetching graph data:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch graph data' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
20
control-plane/src/app/api/list/route.ts
Normal file
20
control-plane/src/app/api/list/route.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const DATAPLANE_URL = process.env.NEXT_PUBLIC_DATAPLANE_API_URL || 'http://localhost:8080';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const response = await fetch(`${DATAPLANE_URL}/api/list?${queryString}`);
|
||||
const data = await response.json();
|
||||
return NextResponse.json(data, { status: response.status });
|
||||
} catch (error) {
|
||||
console.error('Error listing memory units:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to list memory units' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
26
control-plane/src/app/api/memories/batch/route.ts
Normal file
26
control-plane/src/app/api/memories/batch/route.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const DATAPLANE_URL = process.env.NEXT_PUBLIC_DATAPLANE_API_URL || 'http://localhost:8080';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
const response = await fetch(`${DATAPLANE_URL}/api/memories/batch`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
return NextResponse.json(data, { status: response.status });
|
||||
} catch (error) {
|
||||
console.error('Error batch put:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to batch put' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
26
control-plane/src/app/api/memories/batch_async/route.ts
Normal file
26
control-plane/src/app/api/memories/batch_async/route.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const DATAPLANE_URL = process.env.NEXT_PUBLIC_DATAPLANE_API_URL || 'http://localhost:8080';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
const response = await fetch(`${DATAPLANE_URL}/api/memories/batch_async`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
return NextResponse.json(data, { status: response.status });
|
||||
} catch (error) {
|
||||
console.error('Error batch put async:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to batch put async' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
21
control-plane/src/app/api/operations/[agentId]/route.ts
Normal file
21
control-plane/src/app/api/operations/[agentId]/route.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const DATAPLANE_URL = process.env.NEXT_PUBLIC_DATAPLANE_API_URL || 'http://localhost:8080';
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ agentId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { agentId } = await params;
|
||||
const response = await fetch(`${DATAPLANE_URL}/api/operations/${agentId}`);
|
||||
const data = await response.json();
|
||||
return NextResponse.json(data, { status: response.status });
|
||||
} catch (error) {
|
||||
console.error('Error fetching operations:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch operations' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
26
control-plane/src/app/api/search/route.ts
Normal file
26
control-plane/src/app/api/search/route.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const DATAPLANE_URL = process.env.NEXT_PUBLIC_DATAPLANE_API_URL || 'http://localhost:8080';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
const response = await fetch(`${DATAPLANE_URL}/api/search`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
return NextResponse.json(data, { status: response.status });
|
||||
} catch (error) {
|
||||
console.error('Error searching:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to search' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
21
control-plane/src/app/api/stats/[agentId]/route.ts
Normal file
21
control-plane/src/app/api/stats/[agentId]/route.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const DATAPLANE_URL = process.env.NEXT_PUBLIC_DATAPLANE_API_URL || 'http://localhost:8080';
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ agentId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { agentId } = await params;
|
||||
const response = await fetch(`${DATAPLANE_URL}/api/stats/${agentId}`);
|
||||
const data = await response.json();
|
||||
return NextResponse.json(data, { status: response.status });
|
||||
} catch (error) {
|
||||
console.error('Error fetching stats:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch stats' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
26
control-plane/src/app/api/think/route.ts
Normal file
26
control-plane/src/app/api/think/route.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const DATAPLANE_URL = process.env.NEXT_PUBLIC_DATAPLANE_API_URL || 'http://localhost:8080';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
const response = await fetch(`${DATAPLANE_URL}/api/think`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
return NextResponse.json(data, { status: response.status });
|
||||
} catch (error) {
|
||||
console.error('Error thinking:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to think' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
143
control-plane/src/app/dashboard/page.tsx
Normal file
143
control-plane/src/app/dashboard/page.tsx
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { AgentSelector } from '@/components/agent-selector';
|
||||
import { DataView } from '@/components/data-view';
|
||||
import { DocumentsView } from '@/components/documents-view';
|
||||
import { ThinkView } from '@/components/think-view';
|
||||
import { AddMemoryView } from '@/components/add-memory-view';
|
||||
import { StatsView } from '@/components/stats-view';
|
||||
import { SearchDebugView } from '@/components/search-debug-view';
|
||||
import { useAgent } from '@/lib/agent-context';
|
||||
|
||||
type MainTab = 'data' | 'documents' | 'search' | 'stats' | 'think' | 'add';
|
||||
type DataSubTab = 'world' | 'agent' | 'opinion';
|
||||
|
||||
export default function DashboardPage() {
|
||||
const [mainTab, setMainTab] = useState<MainTab>('data');
|
||||
const [dataSubTab, setDataSubTab] = useState<DataSubTab>('world');
|
||||
const { currentAgent } = useAgent();
|
||||
|
||||
const TabButton = ({ tab, label }: { tab: MainTab; label: string }) => (
|
||||
<button
|
||||
onClick={() => setMainTab(tab)}
|
||||
className={`px-6 py-3 font-bold text-base transition-colors border-t-2 border-l-2 border-r-2 ${
|
||||
mainTab === tab
|
||||
? 'bg-background text-foreground border-primary border-b-2 border-b-background -mb-0.5'
|
||||
: 'bg-muted text-muted-foreground border-transparent hover:bg-accent'
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
|
||||
const DataSubTabButton = ({ tab, label }: { tab: DataSubTab; label: string }) => (
|
||||
<button
|
||||
onClick={() => setDataSubTab(tab)}
|
||||
className={`px-5 py-2 font-bold text-sm rounded transition-all border-2 ${
|
||||
dataSubTab === tab
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'bg-background text-foreground border-primary hover:bg-accent'
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
|
||||
const NoAgentMessage = ({ message }: { message: string }) => (
|
||||
<div className="p-10 text-center text-muted-foreground bg-muted">
|
||||
<h3 className="text-xl font-semibold mb-2">No Agent Selected</h3>
|
||||
<p>{message}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<AgentSelector />
|
||||
|
||||
{/* Main Tabs */}
|
||||
<div className="bg-muted border-b-2 border-primary">
|
||||
<TabButton tab="data" label="Data" />
|
||||
<TabButton tab="documents" label="Documents" />
|
||||
<TabButton tab="search" label="Search Debug" />
|
||||
<TabButton tab="stats" label="Stats & Operations" />
|
||||
<TabButton tab="think" label="Think" />
|
||||
<TabButton tab="add" label="Add Memory" />
|
||||
</div>
|
||||
|
||||
{/* Tab Content - All tabs rendered but hidden to preserve state */}
|
||||
<div className="p-5">
|
||||
{/* Data Tab */}
|
||||
<div className={mainTab !== 'data' ? 'hidden' : ''}>
|
||||
{/* Data Sub Tabs */}
|
||||
<div className="bg-accent px-5 py-2.5 border-b-2 border-primary flex gap-2.5">
|
||||
<DataSubTabButton tab="world" label="World" />
|
||||
<DataSubTabButton tab="agent" label="Agent" />
|
||||
<DataSubTabButton tab="opinion" label="Opinions" />
|
||||
</div>
|
||||
|
||||
{/* Data Sub Tab Content - Render all but hide inactive */}
|
||||
<div className="mt-5">
|
||||
{!currentAgent ? (
|
||||
<NoAgentMessage message="Please select an agent from the dropdown above to view data." />
|
||||
) : (
|
||||
<div>
|
||||
<div className={dataSubTab !== 'world' ? 'hidden' : ''}>
|
||||
<DataView factType="world" />
|
||||
</div>
|
||||
<div className={dataSubTab !== 'agent' ? 'hidden' : ''}>
|
||||
<DataView factType="agent" />
|
||||
</div>
|
||||
<div className={dataSubTab !== 'opinion' ? 'hidden' : ''}>
|
||||
<DataView factType="opinion" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Documents Tab */}
|
||||
<div className={mainTab !== 'documents' ? 'hidden' : ''}>
|
||||
<h2 className="text-2xl font-bold mb-4">Documents</h2>
|
||||
{!currentAgent ? (
|
||||
<NoAgentMessage message="Please select an agent from the dropdown above to view documents." />
|
||||
) : (
|
||||
<DocumentsView />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Search Debug Tab */}
|
||||
<div className={mainTab !== 'search' ? 'hidden' : ''}>
|
||||
<h2 className="text-2xl font-bold mb-4">Search Debug</h2>
|
||||
<SearchDebugView />
|
||||
</div>
|
||||
|
||||
{/* Stats Tab */}
|
||||
<div className={mainTab !== 'stats' ? 'hidden' : ''}>
|
||||
<h2 className="text-2xl font-bold mb-4">Statistics & Operations</h2>
|
||||
<StatsView />
|
||||
</div>
|
||||
|
||||
{/* Think Tab */}
|
||||
<div className={mainTab !== 'think' ? 'hidden' : ''}>
|
||||
<h2 className="text-2xl font-bold mb-4">Think - AI-Powered Answers</h2>
|
||||
{!currentAgent ? (
|
||||
<NoAgentMessage message="Please select an agent from the dropdown above to use the think feature." />
|
||||
) : (
|
||||
<ThinkView />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Add Memory Tab */}
|
||||
<div className={mainTab !== 'add' ? 'hidden' : ''}>
|
||||
<h2 className="text-2xl font-bold mb-4">Add Memory</h2>
|
||||
{!currentAgent ? (
|
||||
<NoAgentMessage message="Please select an agent from the dropdown above to add memories." />
|
||||
) : (
|
||||
<AddMemoryView />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
172
control-plane/src/app/globals.css
Normal file
172
control-plane/src/app/globals.css
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
@import "tailwindcss";
|
||||
:root {
|
||||
--background: oklch(1.0000 0 0);
|
||||
--foreground: oklch(0.2101 0.0318 264.6645);
|
||||
--card: oklch(1.0000 0 0);
|
||||
--card-foreground: oklch(0.2101 0.0318 264.6645);
|
||||
--popover: oklch(1.0000 0 0);
|
||||
--popover-foreground: oklch(0.2101 0.0318 264.6645);
|
||||
--primary: oklch(0.6716 0.1368 48.5130);
|
||||
--primary-foreground: oklch(1.0000 0 0);
|
||||
--secondary: oklch(0.5360 0.0398 196.0280);
|
||||
--secondary-foreground: oklch(1.0000 0 0);
|
||||
--muted: oklch(0.9670 0.0029 264.5419);
|
||||
--muted-foreground: oklch(0.5510 0.0234 264.3637);
|
||||
--accent: oklch(0.9491 0 0);
|
||||
--accent-foreground: oklch(0.2101 0.0318 264.6645);
|
||||
--destructive: oklch(0.6368 0.2078 25.3313);
|
||||
--destructive-foreground: oklch(0.9851 0 0);
|
||||
--border: oklch(0.9276 0.0058 264.5313);
|
||||
--input: oklch(0.9276 0.0058 264.5313);
|
||||
--ring: oklch(0.6716 0.1368 48.5130);
|
||||
--chart-1: oklch(0.5940 0.0443 196.0233);
|
||||
--chart-2: oklch(0.7214 0.1337 49.9802);
|
||||
--chart-3: oklch(0.8721 0.0864 68.5474);
|
||||
--chart-4: oklch(0.6268 0 0);
|
||||
--chart-5: oklch(0.6830 0 0);
|
||||
--sidebar: oklch(0.9670 0.0029 264.5419);
|
||||
--sidebar-foreground: oklch(0.2101 0.0318 264.6645);
|
||||
--sidebar-primary: oklch(0.6716 0.1368 48.5130);
|
||||
--sidebar-primary-foreground: oklch(1.0000 0 0);
|
||||
--sidebar-accent: oklch(1.0000 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.2101 0.0318 264.6645);
|
||||
--sidebar-border: oklch(0.9276 0.0058 264.5313);
|
||||
--sidebar-ring: oklch(0.6716 0.1368 48.5130);
|
||||
--font-sans: Geist Mono, ui-monospace, monospace;
|
||||
--font-serif: serif;
|
||||
--font-mono: JetBrains Mono, monospace;
|
||||
--radius: 0.75rem;
|
||||
--shadow-x: 0px;
|
||||
--shadow-y: 1px;
|
||||
--shadow-blur: 4px;
|
||||
--shadow-spread: 0px;
|
||||
--shadow-opacity: 0.05;
|
||||
--shadow-color: #000000;
|
||||
--shadow-2xs: 0px 1px 4px 0px hsl(0 0% 0% / 0.03);
|
||||
--shadow-xs: 0px 1px 4px 0px hsl(0 0% 0% / 0.03);
|
||||
--shadow-sm: 0px 1px 4px 0px hsl(0 0% 0% / 0.05), 0px 1px 2px -1px hsl(0 0% 0% / 0.05);
|
||||
--shadow: 0px 1px 4px 0px hsl(0 0% 0% / 0.05), 0px 1px 2px -1px hsl(0 0% 0% / 0.05);
|
||||
--shadow-md: 0px 1px 4px 0px hsl(0 0% 0% / 0.05), 0px 2px 4px -1px hsl(0 0% 0% / 0.05);
|
||||
--shadow-lg: 0px 1px 4px 0px hsl(0 0% 0% / 0.05), 0px 4px 6px -1px hsl(0 0% 0% / 0.05);
|
||||
--shadow-xl: 0px 1px 4px 0px hsl(0 0% 0% / 0.05), 0px 8px 10px -1px hsl(0 0% 0% / 0.05);
|
||||
--shadow-2xl: 0px 1px 4px 0px hsl(0 0% 0% / 0.13);
|
||||
--tracking-normal: 0rem;
|
||||
--spacing: 0.25rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.1797 0.0043 308.1928);
|
||||
--foreground: oklch(0.8109 0 0);
|
||||
--card: oklch(0.1822 0 0);
|
||||
--card-foreground: oklch(0.8109 0 0);
|
||||
--popover: oklch(0.1797 0.0043 308.1928);
|
||||
--popover-foreground: oklch(0.8109 0 0);
|
||||
--primary: oklch(0.7214 0.1337 49.9802);
|
||||
--primary-foreground: oklch(0.1797 0.0043 308.1928);
|
||||
--secondary: oklch(0.5940 0.0443 196.0233);
|
||||
--secondary-foreground: oklch(0.1797 0.0043 308.1928);
|
||||
--muted: oklch(0.2520 0 0);
|
||||
--muted-foreground: oklch(0.6268 0 0);
|
||||
--accent: oklch(0.3211 0 0);
|
||||
--accent-foreground: oklch(0.8109 0 0);
|
||||
--destructive: oklch(0.5940 0.0443 196.0233);
|
||||
--destructive-foreground: oklch(0.1797 0.0043 308.1928);
|
||||
--border: oklch(0.2520 0 0);
|
||||
--input: oklch(0.2520 0 0);
|
||||
--ring: oklch(0.7214 0.1337 49.9802);
|
||||
--chart-1: oklch(0.5940 0.0443 196.0233);
|
||||
--chart-2: oklch(0.7214 0.1337 49.9802);
|
||||
--chart-3: oklch(0.8721 0.0864 68.5474);
|
||||
--chart-4: oklch(0.6268 0 0);
|
||||
--chart-5: oklch(0.6830 0 0);
|
||||
--sidebar: oklch(0.1822 0 0);
|
||||
--sidebar-foreground: oklch(0.8109 0 0);
|
||||
--sidebar-primary: oklch(0.7214 0.1337 49.9802);
|
||||
--sidebar-primary-foreground: oklch(0.1797 0.0043 308.1928);
|
||||
--sidebar-accent: oklch(0.3211 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.8109 0 0);
|
||||
--sidebar-border: oklch(0.2520 0 0);
|
||||
--sidebar-ring: oklch(0.7214 0.1337 49.9802);
|
||||
--font-sans: Geist Mono, ui-monospace, monospace;
|
||||
--font-serif: serif;
|
||||
--font-mono: JetBrains Mono, monospace;
|
||||
--radius: 0.75rem;
|
||||
--shadow-x: 0px;
|
||||
--shadow-y: 1px;
|
||||
--shadow-blur: 4px;
|
||||
--shadow-spread: 0px;
|
||||
--shadow-opacity: 0.05;
|
||||
--shadow-color: #000000;
|
||||
--shadow-2xs: 0px 1px 4px 0px hsl(0 0% 0% / 0.03);
|
||||
--shadow-xs: 0px 1px 4px 0px hsl(0 0% 0% / 0.03);
|
||||
--shadow-sm: 0px 1px 4px 0px hsl(0 0% 0% / 0.05), 0px 1px 2px -1px hsl(0 0% 0% / 0.05);
|
||||
--shadow: 0px 1px 4px 0px hsl(0 0% 0% / 0.05), 0px 1px 2px -1px hsl(0 0% 0% / 0.05);
|
||||
--shadow-md: 0px 1px 4px 0px hsl(0 0% 0% / 0.05), 0px 2px 4px -1px hsl(0 0% 0% / 0.05);
|
||||
--shadow-lg: 0px 1px 4px 0px hsl(0 0% 0% / 0.05), 0px 4px 6px -1px hsl(0 0% 0% / 0.05);
|
||||
--shadow-xl: 0px 1px 4px 0px hsl(0 0% 0% / 0.05), 0px 8px 10px -1px hsl(0 0% 0% / 0.05);
|
||||
--shadow-2xl: 0px 1px 4px 0px hsl(0 0% 0% / 0.13);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
|
||||
--font-sans: var(--font-sans);
|
||||
--font-mono: var(--font-mono);
|
||||
--font-serif: var(--font-serif);
|
||||
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
|
||||
--shadow-2xs: var(--shadow-2xs);
|
||||
--shadow-xs: var(--shadow-xs);
|
||||
--shadow-sm: var(--shadow-sm);
|
||||
--shadow: var(--shadow);
|
||||
--shadow-md: var(--shadow-md);
|
||||
--shadow-lg: var(--shadow-lg);
|
||||
--shadow-xl: var(--shadow-xl);
|
||||
--shadow-2xl: var(--shadow-2xl);
|
||||
|
||||
--tracking-tighter: calc(var(--tracking-normal) - 0.05em);
|
||||
--tracking-tight: calc(var(--tracking-normal) - 0.025em);
|
||||
--tracking-normal: var(--tracking-normal);
|
||||
--tracking-wide: calc(var(--tracking-normal) + 0.025em);
|
||||
--tracking-wider: calc(var(--tracking-normal) + 0.05em);
|
||||
--tracking-widest: calc(var(--tracking-normal) + 0.1em);
|
||||
}
|
||||
|
||||
body {
|
||||
letter-spacing: var(--tracking-normal);
|
||||
}
|
||||
27
control-plane/src/app/layout.tsx
Normal file
27
control-plane/src/app/layout.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { AgentProvider } from "@/lib/agent-context";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Memory Control Plane",
|
||||
description: "Control plane for the temporal semantic memory system",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>
|
||||
<AgentProvider>
|
||||
{children}
|
||||
</AgentProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
5
control-plane/src/app/page.tsx
Normal file
5
control-plane/src/app/page.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { redirect } from 'next/navigation';
|
||||
|
||||
export default function Home() {
|
||||
redirect('/dashboard');
|
||||
}
|
||||
163
control-plane/src/components/add-memory-view.tsx
Normal file
163
control-plane/src/components/add-memory-view.tsx
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { dataplaneClient } from '@/lib/api';
|
||||
import { useAgent } from '@/lib/agent-context';
|
||||
|
||||
export function AddMemoryView() {
|
||||
const { currentAgent } = useAgent();
|
||||
const [content, setContent] = useState('');
|
||||
const [context, setContext] = useState('');
|
||||
const [eventDate, setEventDate] = useState('');
|
||||
const [documentId, setDocumentId] = useState('');
|
||||
const [async, setAsync] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [result, setResult] = useState<string | null>(null);
|
||||
|
||||
const clearForm = () => {
|
||||
setContent('');
|
||||
setContext('');
|
||||
setEventDate('');
|
||||
setDocumentId('');
|
||||
setAsync(false);
|
||||
setResult(null);
|
||||
};
|
||||
|
||||
const submitMemory = async () => {
|
||||
if (!currentAgent || !content) {
|
||||
alert('Please enter content');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setResult(null);
|
||||
|
||||
try {
|
||||
const item: any = { content };
|
||||
if (context) item.context = context;
|
||||
if (eventDate) item.event_date = eventDate;
|
||||
|
||||
const params: any = {
|
||||
agent_id: currentAgent,
|
||||
items: [item],
|
||||
};
|
||||
|
||||
if (documentId) params.document_id = documentId;
|
||||
|
||||
let data: any;
|
||||
if (async) {
|
||||
data = await dataplaneClient.batchPutAsync(params);
|
||||
} else {
|
||||
data = await dataplaneClient.batchPut(params);
|
||||
}
|
||||
|
||||
setResult(data.message as string);
|
||||
setContent('');
|
||||
} catch (error) {
|
||||
console.error('Error submitting memory:', error);
|
||||
setResult('Error: ' + (error as Error).message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl">
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Submit memories to the selected agent. You can add one or multiple memories at once.
|
||||
</p>
|
||||
|
||||
<div className="max-w-3xl">
|
||||
<div className="bg-card p-5 rounded-lg mb-5 border-2 border-primary">
|
||||
<h3 className="mt-0 text-card-foreground">Memory Entry</h3>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="font-bold block mb-1 text-card-foreground">Content *</label>
|
||||
<textarea
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
placeholder="Enter the memory content..."
|
||||
className="w-full min-h-[100px] px-2.5 py-2 border-2 border-border bg-background text-foreground rounded text-sm resize-y focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="font-bold block mb-1 text-card-foreground">Context</label>
|
||||
<input
|
||||
type="text"
|
||||
value={context}
|
||||
onChange={(e) => setContext(e.target.value)}
|
||||
placeholder="Optional context about this memory..."
|
||||
className="w-full px-2.5 py-2 border-2 border-border bg-background text-foreground rounded text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="font-bold block mb-1 text-card-foreground">Event Date</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={eventDate}
|
||||
onChange={(e) => setEventDate(e.target.value)}
|
||||
className="w-full px-2.5 py-2 border-2 border-border bg-background text-foreground rounded text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="font-bold block mb-1 text-card-foreground">Document ID</label>
|
||||
<input
|
||||
type="text"
|
||||
value={documentId}
|
||||
onChange={(e) => setDocumentId(e.target.value)}
|
||||
placeholder="Optional document identifier (automatically upserts if document exists)..."
|
||||
className="w-full px-2.5 py-2 border-2 border-border bg-background text-foreground rounded text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
<small className="text-muted-foreground text-xs mt-1 block">
|
||||
Note: If a document with this ID already exists, it will be automatically replaced with the new content.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={async}
|
||||
onChange={(e) => setAsync(e.target.checked)}
|
||||
className="mr-2 w-4 h-4 cursor-pointer"
|
||||
/>
|
||||
<span className="font-bold text-card-foreground">Async (process in background)</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2.5">
|
||||
<button
|
||||
onClick={submitMemory}
|
||||
disabled={loading}
|
||||
className="px-6 py-3 bg-primary text-primary-foreground rounded cursor-pointer font-bold text-sm hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? 'Submitting...' : 'Submit Memory'}
|
||||
</button>
|
||||
<button
|
||||
onClick={clearForm}
|
||||
className="px-6 py-3 bg-secondary text-secondary-foreground rounded cursor-pointer font-bold text-sm hover:opacity-90"
|
||||
>
|
||||
Clear Form
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{result && (
|
||||
<div className={`mt-5 p-5 rounded-lg border-2 ${result.startsWith('Error') ? 'bg-destructive/10 border-destructive text-destructive' : 'bg-primary/10 border-primary text-primary'}`}>
|
||||
<div className="font-semibold">{result}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading && (
|
||||
<div className="text-center py-10 text-muted-foreground">
|
||||
<div className="text-5xl mb-2.5">⏳</div>
|
||||
<div className="text-lg">Submitting memory...</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
36
control-plane/src/components/agent-selector.tsx
Normal file
36
control-plane/src/components/agent-selector.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
'use client';
|
||||
|
||||
import { useAgent } from '@/lib/agent-context';
|
||||
|
||||
export function AgentSelector() {
|
||||
const { currentAgent, setCurrentAgent, agents, loadAgents } = useAgent();
|
||||
|
||||
return (
|
||||
<div className="bg-card text-card-foreground px-5 py-3 border-b-4 border-primary">
|
||||
<div className="flex items-center gap-2.5 text-sm">
|
||||
<span className="font-medium">Memory Graph</span>
|
||||
<span className="text-muted-foreground font-bold">/</span>
|
||||
<span className="font-medium">Agent:</span>
|
||||
<select
|
||||
value={currentAgent || ''}
|
||||
onChange={(e) => setCurrentAgent(e.target.value || null)}
|
||||
className="px-3 py-1.5 border-2 border-primary rounded bg-background text-foreground text-sm font-bold cursor-pointer transition-all hover:bg-accent focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
>
|
||||
<option value="">Select an agent...</option>
|
||||
{agents.map((agent) => (
|
||||
<option key={agent} value={agent}>
|
||||
{agent}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
onClick={loadAgents}
|
||||
className="ml-2 px-2 py-1 text-xs bg-primary text-primary-foreground hover:opacity-90 rounded transition-colors"
|
||||
title="Refresh agents"
|
||||
>
|
||||
🔄
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
311
control-plane/src/components/data-view.tsx
Normal file
311
control-plane/src/components/data-view.tsx
Normal file
|
|
@ -0,0 +1,311 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { dataplaneClient } from '@/lib/api';
|
||||
import { useAgent } from '@/lib/agent-context';
|
||||
import cytoscape from 'cytoscape';
|
||||
|
||||
type FactType = 'world' | 'agent' | 'opinion';
|
||||
type ViewMode = 'graph' | 'table';
|
||||
|
||||
interface DataViewProps {
|
||||
factType: FactType;
|
||||
}
|
||||
|
||||
export function DataView({ factType }: DataViewProps) {
|
||||
const { currentAgent } = useAgent();
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('graph');
|
||||
const [data, setData] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [nodeLimit, setNodeLimit] = useState(50);
|
||||
const [layout, setLayout] = useState('circle');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const cyRef = useRef<any>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const loadData = async () => {
|
||||
if (!currentAgent) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const graphData: any = await dataplaneClient.getGraphData({
|
||||
agent_id: currentAgent,
|
||||
fact_type: factType,
|
||||
});
|
||||
console.log('Loaded graph data:', {
|
||||
total_units: graphData.total_units,
|
||||
nodes: graphData.nodes?.length,
|
||||
edges: graphData.edges?.length,
|
||||
table_rows: graphData.table_rows?.length,
|
||||
});
|
||||
setData(graphData);
|
||||
} catch (error) {
|
||||
console.error('Error loading data:', error);
|
||||
alert(`Error loading ${factType} data: ` + (error as Error).message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderGraph = () => {
|
||||
if (!data || !containerRef.current || !data.nodes || !data.edges) return;
|
||||
|
||||
if (cyRef.current) {
|
||||
cyRef.current.destroy();
|
||||
}
|
||||
|
||||
const limitedNodes = (data.nodes || []).slice(0, nodeLimit);
|
||||
const nodeIds = new Set(limitedNodes.map((n: any) => n.data.id));
|
||||
const limitedEdges = (data.edges || []).filter((e: any) =>
|
||||
nodeIds.has(e.data.source) && nodeIds.has(e.data.target)
|
||||
);
|
||||
|
||||
const layouts: any = {
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
||||
cyRef.current = cytoscape({
|
||||
container: containerRef.current,
|
||||
elements: [
|
||||
...limitedNodes.map((n: any) => ({ data: n.data })),
|
||||
...limitedEdges.map((e: any) => ({ data: e.data })),
|
||||
],
|
||||
style: [
|
||||
{
|
||||
selector: 'node',
|
||||
style: {
|
||||
'background-color': 'data(color)' as any,
|
||||
label: 'data(label)' as any,
|
||||
'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)' as any,
|
||||
'line-style': 'data(lineStyle)' as any,
|
||||
'target-arrow-shape': 'triangle',
|
||||
'target-arrow-color': 'data(color)' as any,
|
||||
'curve-style': 'bezier',
|
||||
opacity: 0.7,
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: 'node:selected',
|
||||
style: {
|
||||
'border-width': 4,
|
||||
'border-color': '#000',
|
||||
},
|
||||
},
|
||||
] as any,
|
||||
layout: layouts[layout] || layouts.circle,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (viewMode === 'graph' && data) {
|
||||
renderGraph();
|
||||
}
|
||||
}, [viewMode, data, nodeLimit, layout]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-4 p-2.5 bg-card rounded-lg border-2 border-primary flex gap-4 items-center flex-wrap">
|
||||
<button
|
||||
onClick={loadData}
|
||||
disabled={loading}
|
||||
className="px-5 py-2 bg-primary text-primary-foreground rounded font-bold text-sm hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? '⏳ Loading...' : data ? `🔄 Refresh ${factType.charAt(0).toUpperCase() + factType.slice(1)} Facts` : `📊 Load ${factType.charAt(0).toUpperCase() + factType.slice(1)} Facts`}
|
||||
</button>
|
||||
{data && (
|
||||
<span className="text-muted-foreground text-sm">
|
||||
({data.total_units} total facts)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{data && (
|
||||
<>
|
||||
<div className="bg-accent px-5 py-2.5 border-b-2 border-primary flex gap-2.5 mb-4">
|
||||
<button
|
||||
onClick={() => setViewMode('graph')}
|
||||
className={`px-4 py-1.5 font-bold text-sm rounded transition-all border-2 ${
|
||||
viewMode === 'graph'
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'bg-background text-foreground border-border hover:bg-muted'
|
||||
}`}
|
||||
>
|
||||
Graph
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('table')}
|
||||
className={`px-4 py-1.5 font-bold text-sm rounded transition-all border-2 ${
|
||||
viewMode === 'table'
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'bg-background text-foreground border-border hover:bg-muted'
|
||||
}`}
|
||||
>
|
||||
Table
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{viewMode === 'graph' && (
|
||||
<div className="relative">
|
||||
<div className="p-4 bg-card border-b-2 border-primary flex gap-4 items-center flex-wrap">
|
||||
<div>
|
||||
<label className="mr-2 font-semibold text-card-foreground">Limit nodes:</label>
|
||||
<input
|
||||
type="number"
|
||||
value={nodeLimit}
|
||||
onChange={(e) => setNodeLimit(parseInt(e.target.value))}
|
||||
min="10"
|
||||
max="1000"
|
||||
step="10"
|
||||
className="w-20 px-2 py-1 border-2 border-border bg-background text-foreground rounded focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mr-2 font-semibold text-card-foreground">Layout:</label>
|
||||
<select
|
||||
value={layout}
|
||||
onChange={(e) => setLayout(e.target.value)}
|
||||
className="px-2 py-1 border-2 border-border bg-background text-foreground rounded focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
>
|
||||
<option value="circle">Circle (fast)</option>
|
||||
<option value="grid">Grid (fast)</option>
|
||||
<option value="cose">Force-directed (slow)</option>
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
onClick={renderGraph}
|
||||
className="px-4 py-1.5 bg-secondary text-secondary-foreground rounded font-bold hover:opacity-90"
|
||||
>
|
||||
Apply Layout
|
||||
</button>
|
||||
</div>
|
||||
<div ref={containerRef} className="w-full h-[800px] bg-background" />
|
||||
<div className="absolute top-20 left-5 bg-card p-4 border-2 border-primary rounded-lg shadow-lg max-w-[250px]">
|
||||
<h3 className="font-bold mb-2 border-b-2 border-primary pb-1 text-card-foreground">Legend</h3>
|
||||
<h4 className="font-bold mt-2 mb-1 text-sm text-card-foreground">Link Types:</h4>
|
||||
<div className="flex items-center my-2">
|
||||
<div className="w-8 h-0.5 mr-2.5 bg-cyan-500 border-t border-dashed border-cyan-500" />
|
||||
<span className="text-sm"><strong>Temporal</strong></span>
|
||||
</div>
|
||||
<div className="flex items-center my-2">
|
||||
<div className="w-8 h-0.5 mr-2.5 bg-pink-500" />
|
||||
<span className="text-sm"><strong>Semantic</strong></span>
|
||||
</div>
|
||||
<div className="flex items-center my-2">
|
||||
<div className="w-8 h-0.5 mr-2.5 bg-yellow-500" />
|
||||
<span className="text-sm"><strong>Entity</strong></span>
|
||||
</div>
|
||||
<h4 className="font-bold mt-2 mb-1 text-sm">Nodes:</h4>
|
||||
<div className="flex items-center my-2">
|
||||
<div className="w-5 h-5 mr-2.5 bg-gray-300 border border-gray-500 rounded" />
|
||||
<span className="text-sm">No entities</span>
|
||||
</div>
|
||||
<div className="flex items-center my-2">
|
||||
<div className="w-5 h-5 mr-2.5 bg-blue-300 border border-gray-500 rounded" />
|
||||
<span className="text-sm">1 entity</span>
|
||||
</div>
|
||||
<div className="flex items-center my-2">
|
||||
<div className="w-5 h-5 mr-2.5 bg-blue-500 border border-gray-500 rounded" />
|
||||
<span className="text-sm">2+ entities</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{viewMode === 'table' && (
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Search memories (text, context)..."
|
||||
className="w-full max-w-2xl px-2.5 py-2 mb-4 mx-5 border-2 border-border bg-background text-foreground rounded text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
<div className="overflow-x-auto px-5 pb-5">
|
||||
<table className="w-full border-collapse text-xs max-w-7xl">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="p-2.5 text-left border border-border bg-card text-card-foreground">ID</th>
|
||||
<th className="p-2.5 text-left border border-border bg-card text-card-foreground">Text</th>
|
||||
<th className="p-2.5 text-left border border-border bg-card text-card-foreground">Context</th>
|
||||
<th className="p-2.5 text-left border border-border bg-card text-card-foreground">Date</th>
|
||||
<th className="p-2.5 text-left border border-border bg-card text-card-foreground">Entities</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.table_rows && data.table_rows.length > 0 ? (
|
||||
data.table_rows
|
||||
.filter((row: any) => {
|
||||
if (!searchQuery) return true;
|
||||
const query = searchQuery.toLowerCase();
|
||||
return (
|
||||
row.text?.toLowerCase().includes(query) ||
|
||||
row.context?.toLowerCase().includes(query)
|
||||
);
|
||||
})
|
||||
.map((row: any, idx: number) => (
|
||||
<tr key={idx} className="bg-background hover:bg-muted">
|
||||
<td className="p-2 border border-border" title={row.id}>{row.id}</td>
|
||||
<td className="p-2 border border-border">{row.text}</td>
|
||||
<td className="p-2 border border-border">{row.context || 'N/A'}</td>
|
||||
<td className="p-2 border border-border">{row.date || 'N/A'}</td>
|
||||
<td className="p-2 border border-border">{row.entities || 'None'}</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={5} className="p-10 text-center text-muted-foreground bg-muted">
|
||||
{data.table_rows ? 'No facts match your search' : 'No facts found for this agent and fact type'}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
125
control-plane/src/components/documents-view.tsx
Normal file
125
control-plane/src/components/documents-view.tsx
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { dataplaneClient } from '@/lib/api';
|
||||
import { useAgent } from '@/lib/agent-context';
|
||||
|
||||
export function DocumentsView() {
|
||||
const { currentAgent } = useAgent();
|
||||
const [documents, setDocuments] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [total, setTotal] = useState(0);
|
||||
|
||||
const loadDocuments = async () => {
|
||||
if (!currentAgent) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const data: any = await dataplaneClient.listDocuments({
|
||||
agent_id: currentAgent,
|
||||
q: searchQuery,
|
||||
limit: 100,
|
||||
});
|
||||
setDocuments(data.items || []);
|
||||
setTotal(data.total || 0);
|
||||
} catch (error) {
|
||||
console.error('Error loading documents:', error);
|
||||
alert('Error loading documents: ' + (error as Error).message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const viewDocumentText = async (documentId: string) => {
|
||||
if (!currentAgent) return;
|
||||
|
||||
try {
|
||||
const doc: any = await dataplaneClient.getDocument(documentId, currentAgent);
|
||||
alert(`Document: ${doc.id}\n\nCreated: ${doc.created_at}\nMemory Units: ${doc.memory_unit_count}\n\n${doc.original_text}`);
|
||||
} catch (error) {
|
||||
console.error('Error loading document:', error);
|
||||
alert('Error loading document: ' + (error as Error).message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-4 p-2.5 bg-card rounded-lg border-2 border-primary flex gap-4 items-center flex-wrap">
|
||||
<button
|
||||
onClick={loadDocuments}
|
||||
disabled={loading}
|
||||
className="px-5 py-2 bg-primary text-primary-foreground rounded font-bold text-sm hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? '⏳ Loading...' : documents.length > 0 ? '🔄 Refresh Documents' : '📄 Load Documents'}
|
||||
</button>
|
||||
{documents.length > 0 && (
|
||||
<span className="text-muted-foreground text-sm">({total} total documents)</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Search documents (ID, metadata)..."
|
||||
className="w-full max-w-2xl px-2.5 py-2 mb-4 mx-5 border-2 border-border bg-background text-foreground rounded text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
|
||||
<div className="overflow-x-auto px-5 pb-5">
|
||||
<table className="w-full border-collapse text-xs max-w-7xl">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="p-2.5 text-left border border-border bg-card text-card-foreground">Document ID</th>
|
||||
<th className="p-2.5 text-left border border-border bg-card text-card-foreground">Created</th>
|
||||
<th className="p-2.5 text-left border border-border bg-card text-card-foreground">Updated</th>
|
||||
<th className="p-2.5 text-left border border-border bg-card text-card-foreground">Text Length</th>
|
||||
<th className="p-2.5 text-left border border-border bg-card text-card-foreground">Memory Units</th>
|
||||
<th className="p-2.5 text-left border border-border bg-card text-card-foreground">Metadata</th>
|
||||
<th className="p-2.5 text-left border border-border bg-card text-card-foreground">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{documents.length > 0 ? (
|
||||
documents.map((doc) => (
|
||||
<tr key={doc.id} className="bg-background hover:bg-muted">
|
||||
<td className="p-2 border border-border" title={doc.id}>
|
||||
{doc.id.length > 30 ? doc.id.substring(0, 30) + '...' : doc.id}
|
||||
</td>
|
||||
<td className="p-2 border border-border">
|
||||
{doc.created_at ? new Date(doc.created_at).toLocaleString() : 'N/A'}
|
||||
</td>
|
||||
<td className="p-2 border border-border">
|
||||
{doc.updated_at ? new Date(doc.updated_at).toLocaleString() : 'N/A'}
|
||||
</td>
|
||||
<td className="p-2 border border-border">{doc.text_length?.toLocaleString()} chars</td>
|
||||
<td className="p-2 border border-border">{doc.memory_unit_count}</td>
|
||||
<td className="p-2 border border-border" title={JSON.stringify(doc.metadata)}>
|
||||
{Object.keys(doc.metadata || {}).length > 0
|
||||
? JSON.stringify(doc.metadata).substring(0, 50) + '...'
|
||||
: 'None'}
|
||||
</td>
|
||||
<td className="p-2 border border-border">
|
||||
<button
|
||||
onClick={() => viewDocumentText(doc.id)}
|
||||
className="px-2.5 py-1 bg-primary text-primary-foreground rounded text-xs font-bold hover:opacity-90"
|
||||
title="View original text"
|
||||
>
|
||||
View Text
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={7} className="p-10 text-center text-muted-foreground bg-muted">
|
||||
Click "Load Documents" to view data
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
701
control-plane/src/components/search-debug-view.tsx
Normal file
701
control-plane/src/components/search-debug-view.tsx
Normal file
|
|
@ -0,0 +1,701 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { dataplaneClient } from '@/lib/api';
|
||||
import { useAgent } from '@/lib/agent-context';
|
||||
|
||||
type Phase = 'retrieval' | 'rrf' | 'rerank' | 'final';
|
||||
type RetrievalMethod = 'semantic' | 'bm25' | 'graph' | 'temporal';
|
||||
type FactType = 'world' | 'agent' | 'opinion';
|
||||
|
||||
interface SearchPane {
|
||||
id: number;
|
||||
query: string;
|
||||
factTypes: FactType[];
|
||||
thinkingBudget: number;
|
||||
reranker: string;
|
||||
maxTokens: number;
|
||||
results: any[] | null;
|
||||
trace: any | null;
|
||||
loading: boolean;
|
||||
currentPhase: Phase;
|
||||
currentRetrievalMethod: RetrievalMethod;
|
||||
currentRetrievalFactType: FactType | null;
|
||||
}
|
||||
|
||||
export function SearchDebugView() {
|
||||
const { currentAgent } = useAgent();
|
||||
const [panes, setPanes] = useState<SearchPane[]>([
|
||||
{
|
||||
id: 1,
|
||||
query: '',
|
||||
factTypes: ['world'],
|
||||
thinkingBudget: 100,
|
||||
reranker: 'heuristic',
|
||||
maxTokens: 4096,
|
||||
results: null,
|
||||
trace: null,
|
||||
loading: false,
|
||||
currentPhase: 'retrieval',
|
||||
currentRetrievalMethod: 'semantic',
|
||||
currentRetrievalFactType: null,
|
||||
},
|
||||
]);
|
||||
const [nextPaneId, setNextPaneId] = useState(2);
|
||||
|
||||
const addPane = () => {
|
||||
setPanes([
|
||||
...panes,
|
||||
{
|
||||
id: nextPaneId,
|
||||
query: '',
|
||||
factTypes: ['world'],
|
||||
thinkingBudget: 100,
|
||||
reranker: 'heuristic',
|
||||
maxTokens: 4096,
|
||||
results: null,
|
||||
trace: null,
|
||||
loading: false,
|
||||
currentPhase: 'retrieval',
|
||||
currentRetrievalMethod: 'semantic',
|
||||
currentRetrievalFactType: null,
|
||||
},
|
||||
]);
|
||||
setNextPaneId(nextPaneId + 1);
|
||||
};
|
||||
|
||||
const removePane = (id: number) => {
|
||||
if (panes.length > 1) {
|
||||
setPanes(panes.filter((p) => p.id !== id));
|
||||
}
|
||||
};
|
||||
|
||||
const updatePane = (id: number, updates: Partial<SearchPane>) => {
|
||||
setPanes(panes.map((p) => (p.id === id ? { ...p, ...updates } : p)));
|
||||
};
|
||||
|
||||
const runSearch = async (paneId: number) => {
|
||||
if (!currentAgent) {
|
||||
alert('Please select an agent first');
|
||||
return;
|
||||
}
|
||||
|
||||
const pane = panes.find((p) => p.id === paneId);
|
||||
if (!pane || !pane.query || pane.factTypes.length === 0) {
|
||||
if (pane?.factTypes.length === 0) {
|
||||
alert('Please select at least one fact type');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
updatePane(paneId, { loading: true });
|
||||
|
||||
try {
|
||||
// Always pass fact types as array for consistent behavior
|
||||
const data: any = await dataplaneClient.search({
|
||||
query: pane.query,
|
||||
fact_type: pane.factTypes,
|
||||
agent_id: currentAgent,
|
||||
thinking_budget: pane.thinkingBudget,
|
||||
max_tokens: pane.maxTokens,
|
||||
reranker: pane.reranker,
|
||||
trace: true,
|
||||
});
|
||||
|
||||
// Set default fact type for retrieval view (first selected fact type)
|
||||
const defaultFactType = pane.currentRetrievalFactType || pane.factTypes[0];
|
||||
|
||||
updatePane(paneId, {
|
||||
results: data.results || [],
|
||||
trace: data.trace || null,
|
||||
loading: false,
|
||||
currentRetrievalFactType: defaultFactType,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error running search:', error);
|
||||
alert('Error running search: ' + (error as Error).message);
|
||||
updatePane(paneId, { loading: false });
|
||||
}
|
||||
};
|
||||
|
||||
const renderRetrievalResults = (pane: SearchPane) => {
|
||||
if (!pane.trace || !pane.trace.retrieval_results) {
|
||||
return <div className="p-5 text-center text-muted-foreground">No retrieval data available</div>;
|
||||
}
|
||||
|
||||
// Filter by fact type and method
|
||||
// Since we always send fact types as array, the dataplane should always include fact_type in results
|
||||
const methodData = pane.trace.retrieval_results.find(
|
||||
(m: any) =>
|
||||
m.method_name === pane.currentRetrievalMethod &&
|
||||
(pane.currentRetrievalFactType === null ||
|
||||
!pane.currentRetrievalFactType ||
|
||||
m.fact_type === pane.currentRetrievalFactType)
|
||||
);
|
||||
|
||||
if (!methodData || !methodData.results || methodData.results.length === 0) {
|
||||
return (
|
||||
<div className="p-5 text-center text-muted-foreground">
|
||||
No results from this retrieval method
|
||||
{pane.currentRetrievalFactType && ` for fact type: ${pane.currentRetrievalFactType}`}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4 overflow-auto">
|
||||
<h3 className="text-base font-bold mb-2 text-foreground">
|
||||
{methodData.method_name.toUpperCase()} Retrieval
|
||||
{methodData.fact_type && (
|
||||
<span className="ml-2 text-sm font-normal bg-secondary/30 px-2 py-0.5 rounded">
|
||||
{methodData.fact_type}
|
||||
</span>
|
||||
)}
|
||||
{' '}({methodData.results.length} results, {methodData.duration_seconds?.toFixed(3)}s)
|
||||
</h3>
|
||||
<table className="w-full border-collapse text-xs">
|
||||
<thead>
|
||||
<tr className="bg-card border-2 border-primary">
|
||||
<th className="p-2 text-left border border-border text-card-foreground">Rank</th>
|
||||
<th className="p-2 text-left border border-border text-card-foreground">Text</th>
|
||||
<th className="p-2 text-left border border-border text-card-foreground">Score</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{methodData.results.map((result: any, idx: number) => (
|
||||
<tr key={idx} className="border border-border bg-background">
|
||||
<td className="p-2 border border-border font-bold">#{result.rank}</td>
|
||||
<td className="p-2 border border-border max-w-md">{result.text}</td>
|
||||
<td className="p-2 border border-border">{result.score?.toFixed(4)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderRRFMerge = (pane: SearchPane) => {
|
||||
if (!pane.trace || !pane.trace.rrf_merged || pane.trace.rrf_merged.length === 0) {
|
||||
return <div className="p-5 text-center text-muted-foreground">No RRF merge data available</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4 overflow-auto">
|
||||
<h3 className="text-base font-bold mb-2 text-foreground">
|
||||
RRF Merge Results ({pane.trace.rrf_merged.length} candidates)
|
||||
{pane.factTypes.length > 1 && (
|
||||
<span className="ml-2 text-sm font-normal bg-primary/20 px-2 py-0.5 rounded">
|
||||
Unified across all fact types
|
||||
</span>
|
||||
)}
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">
|
||||
Reciprocal Rank Fusion combines rankings from different retrieval methods
|
||||
{pane.factTypes.length > 1 ? ' and fact types' : ''}.
|
||||
</p>
|
||||
<table className="w-full border-collapse text-xs">
|
||||
<thead>
|
||||
<tr className="bg-card border-2 border-primary">
|
||||
<th className="p-2 text-left border border-border text-card-foreground">RRF Rank</th>
|
||||
<th className="p-2 text-left border border-border text-card-foreground">Text</th>
|
||||
<th className="p-2 text-left border border-border text-card-foreground">RRF Score</th>
|
||||
<th className="p-2 text-left border border-border text-card-foreground">Source Ranks</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{pane.trace.rrf_merged.map((result: any, idx: number) => {
|
||||
const sourceRanks = Object.entries(result.source_ranks || {})
|
||||
.map(([method, rank]) => `${method}: #${rank}`)
|
||||
.join(', ');
|
||||
|
||||
return (
|
||||
<tr key={idx} className="border border-border bg-background">
|
||||
<td className="p-2 border border-border font-bold">
|
||||
#{result.final_rrf_rank}
|
||||
</td>
|
||||
<td className="p-2 border border-border max-w-md">{result.text}</td>
|
||||
<td className="p-2 border border-border">{result.rrf_score?.toFixed(4)}</td>
|
||||
<td className="p-2 border border-border text-xs">{sourceRanks}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderReranking = (pane: SearchPane) => {
|
||||
if (!pane.trace || !pane.trace.reranked || pane.trace.reranked.length === 0) {
|
||||
return <div className="p-5 text-center text-muted-foreground">No reranking data available</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4 overflow-auto">
|
||||
<h3 className="text-base font-bold mb-2 text-foreground">
|
||||
Reranking Results ({pane.trace.reranked.length} results)
|
||||
{pane.factTypes.length > 1 && (
|
||||
<span className="ml-2 text-sm font-normal bg-primary/20 px-2 py-0.5 rounded">
|
||||
Unified across all fact types
|
||||
</span>
|
||||
)}
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">
|
||||
Reranker adjusts scores based on semantic similarity, BM25, recency, and frequency.{' '}
|
||||
<span className="bg-secondary/30 px-2 py-0.5 rounded">Highlight</span> = rank improved
|
||||
vs RRF
|
||||
</p>
|
||||
<table className="w-full border-collapse text-xs">
|
||||
<thead>
|
||||
<tr className="bg-card border-2 border-primary">
|
||||
<th className="p-2 text-left border border-border text-card-foreground">Rerank</th>
|
||||
<th className="p-2 text-left border border-border text-card-foreground">RRF Rank</th>
|
||||
<th className="p-2 text-left border border-border text-card-foreground">Change</th>
|
||||
<th className="p-2 text-left border border-border text-card-foreground">Text</th>
|
||||
<th className="p-2 text-left border border-border text-card-foreground">Score</th>
|
||||
<th className="p-2 text-left border border-border text-card-foreground">Components</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{pane.trace.reranked.map((result: any, idx: number) => {
|
||||
const improved = result.rank_change > 0;
|
||||
const rowBg = improved ? 'bg-secondary/20' : 'bg-background';
|
||||
const changeDisplay =
|
||||
result.rank_change > 0
|
||||
? `↑${result.rank_change}`
|
||||
: result.rank_change < 0
|
||||
? `↓${Math.abs(result.rank_change)}`
|
||||
: '=';
|
||||
const changeColor =
|
||||
result.rank_change > 0
|
||||
? 'text-green-700'
|
||||
: result.rank_change < 0
|
||||
? 'text-red-700'
|
||||
: 'text-gray-600';
|
||||
|
||||
const components = Object.entries(result.score_components || {})
|
||||
.map(([key, val]: [string, any]) => `${key.replace('_', ' ')}: ${val.toFixed(3)}`)
|
||||
.join(', ');
|
||||
|
||||
return (
|
||||
<tr key={idx} className={`border border-border ${rowBg}`}>
|
||||
<td className="p-2 border border-border font-bold">#{result.rerank_rank}</td>
|
||||
<td className="p-2 border border-border">#{result.rrf_rank}</td>
|
||||
<td className={`p-2 border border-border font-bold ${changeColor}`}>
|
||||
{changeDisplay}
|
||||
</td>
|
||||
<td className="p-2 border border-border max-w-sm">{result.text}</td>
|
||||
<td className="p-2 border border-border font-bold">
|
||||
{result.rerank_score?.toFixed(4)}
|
||||
</td>
|
||||
<td className="p-2 border border-border text-xs">{components}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderFinalResults = (pane: SearchPane) => {
|
||||
if (!pane.results || pane.results.length === 0) {
|
||||
return <div className="p-5 text-center text-muted-foreground">No final results</div>;
|
||||
}
|
||||
|
||||
const calculateRanks = (values: number[]) => {
|
||||
const indexed = values.map((val, idx) => ({ idx, val }));
|
||||
indexed.sort((a, b) => b.val - a.val);
|
||||
const ranks = new Map();
|
||||
indexed.forEach((item, rank) => {
|
||||
ranks.set(item.idx, rank + 1);
|
||||
});
|
||||
return ranks;
|
||||
};
|
||||
|
||||
const activations = pane.results.map((result: any) => {
|
||||
const visit = pane.trace?.visits?.find((v: any) => v.node_id === result.id);
|
||||
return visit ? visit.weights.activation : 0;
|
||||
});
|
||||
|
||||
const similarities = pane.results.map((result: any) => {
|
||||
const visit = pane.trace?.visits?.find((v: any) => v.node_id === result.id);
|
||||
return visit ? visit.weights.semantic_similarity : 0;
|
||||
});
|
||||
|
||||
const recencies = pane.results.map((result: any) => {
|
||||
const visit = pane.trace?.visits?.find((v: any) => v.node_id === result.id);
|
||||
return visit ? visit.weights.recency || 0 : 0;
|
||||
});
|
||||
|
||||
const frequencies = pane.results.map((result: any) => {
|
||||
const visit = pane.trace?.visits?.find((v: any) => v.node_id === result.id);
|
||||
return visit ? visit.weights.frequency || 0 : 0;
|
||||
});
|
||||
|
||||
const activationRanks = calculateRanks(activations);
|
||||
const similarityRanks = calculateRanks(similarities);
|
||||
const recencyRanks = calculateRanks(recencies);
|
||||
const frequencyRanks = calculateRanks(frequencies);
|
||||
|
||||
return (
|
||||
<div className="p-4 overflow-auto">
|
||||
<h3 className="text-base font-bold mb-2 text-foreground">
|
||||
Final Results ({pane.results.length} memories)
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">
|
||||
Query: "{pane.trace?.query?.query_text || pane.query}"
|
||||
</p>
|
||||
<table className="w-full border-collapse text-xs">
|
||||
<thead>
|
||||
<tr className="bg-card border-2 border-primary">
|
||||
<th className="p-2 text-left border border-border text-card-foreground">Rank</th>
|
||||
<th className="p-2 text-left border border-border text-card-foreground">Text</th>
|
||||
<th className="p-2 text-left border border-border text-card-foreground">Context</th>
|
||||
<th className="p-2 text-left border border-border text-card-foreground">Date</th>
|
||||
<th className="p-2 text-left border border-border text-card-foreground" title="Final weighted score">
|
||||
Final Score
|
||||
</th>
|
||||
<th className="p-2 text-left border border-border text-card-foreground" title="Spreading activation value">
|
||||
Activation
|
||||
</th>
|
||||
<th className="p-2 text-left border border-border text-card-foreground" title="Semantic similarity to query">
|
||||
Similarity
|
||||
</th>
|
||||
<th className="p-2 text-left border border-border text-card-foreground" title="Recency boost">
|
||||
Recency
|
||||
</th>
|
||||
<th className="p-2 text-left border border-border text-card-foreground" title="Frequency boost">
|
||||
Frequency
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{pane.results.map((result: any, idx: number) => {
|
||||
const visit = pane.trace?.visits?.find((v: any) => v.node_id === result.id);
|
||||
const finalScore = visit ? visit.weights.final_weight : result.score || 0;
|
||||
const activation = visit ? visit.weights.activation : 0;
|
||||
const similarity = visit ? visit.weights.semantic_similarity : 0;
|
||||
const recency = visit ? visit.weights.recency || 0 : 0;
|
||||
const frequency = visit ? visit.weights.frequency || 0 : 0;
|
||||
|
||||
return (
|
||||
<tr key={idx} className="border border-border bg-background">
|
||||
<td className="p-2 border border-border font-bold">#{idx + 1}</td>
|
||||
<td className="p-2 border border-border max-w-xs">{result.text}</td>
|
||||
<td className="p-2 border border-border max-w-32">
|
||||
{result.context || 'N/A'}
|
||||
</td>
|
||||
<td className="p-2 border border-border whitespace-nowrap">
|
||||
{result.event_date
|
||||
? new Date(result.event_date).toLocaleDateString()
|
||||
: 'N/A'}
|
||||
</td>
|
||||
<td className="p-2 border border-border font-bold">
|
||||
{finalScore.toFixed(4)}
|
||||
</td>
|
||||
<td className="p-2 border border-border">
|
||||
{activation.toFixed(4)}{' '}
|
||||
<span className="text-muted-foreground text-xs">(#{activationRanks.get(idx)})</span>
|
||||
</td>
|
||||
<td className="p-2 border border-border">
|
||||
{similarity.toFixed(4)}{' '}
|
||||
<span className="text-muted-foreground text-xs">(#{similarityRanks.get(idx)})</span>
|
||||
</td>
|
||||
<td className="p-2 border border-border">
|
||||
{recency.toFixed(4)}{' '}
|
||||
<span className="text-muted-foreground text-xs">(#{recencyRanks.get(idx)})</span>
|
||||
</td>
|
||||
<td className="p-2 border border-border">
|
||||
{frequency.toFixed(4)}{' '}
|
||||
<span className="text-muted-foreground text-xs">(#{frequencyRanks.get(idx)})</span>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (!currentAgent) {
|
||||
return (
|
||||
<div className="p-10 text-center text-muted-foreground bg-muted rounded-lg">
|
||||
<h3 className="text-xl font-semibold mb-2">No Agent Selected</h3>
|
||||
<p>Please select an agent from the dropdown above to use search debug.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
<button
|
||||
onClick={addPane}
|
||||
className="px-5 py-2 bg-secondary text-secondary-foreground rounded font-bold text-sm hover:opacity-90"
|
||||
>
|
||||
+ Add Search Pane
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
{panes.map((pane) => (
|
||||
<div key={pane.id} className="border-2 border-primary rounded-lg overflow-hidden flex flex-col shadow-md">
|
||||
{/* Header */}
|
||||
<div className="bg-card p-2.5 border-b-2 border-primary font-bold flex justify-between items-center">
|
||||
<span className="text-card-foreground">Search Trace #{pane.id}</span>
|
||||
{panes.length > 1 && (
|
||||
<button
|
||||
onClick={() => removePane(pane.id)}
|
||||
className="px-3 py-1 bg-destructive text-destructive-foreground rounded text-xs hover:opacity-90"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Search Controls */}
|
||||
<div className="p-2.5 bg-accent border-b-2 border-primary">
|
||||
<div className="flex gap-2 flex-wrap items-end">
|
||||
<div>
|
||||
<label className="block text-xs font-bold mb-1 text-accent-foreground">Query:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={pane.query}
|
||||
onChange={(e) => updatePane(pane.id, { query: e.target.value })}
|
||||
placeholder="Enter search query..."
|
||||
className="w-64 px-2 py-1 border-2 border-border bg-background text-foreground rounded text-xs focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
onKeyDown={(e) => e.key === 'Enter' && runSearch(pane.id)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-bold mb-1 text-accent-foreground">Fact Types:</label>
|
||||
<div className="flex gap-3">
|
||||
{(['world', 'agent', 'opinion'] as FactType[]).map((ft) => (
|
||||
<label key={ft} className="flex items-center gap-1 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={pane.factTypes.includes(ft)}
|
||||
onChange={(e) => {
|
||||
const newFactTypes = e.target.checked
|
||||
? [...pane.factTypes, ft]
|
||||
: pane.factTypes.filter((t) => t !== ft);
|
||||
updatePane(pane.id, { factTypes: newFactTypes });
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
<span className="text-xs">{ft.charAt(0).toUpperCase() + ft.slice(1)}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-bold mb-1 text-accent-foreground">Reranker:</label>
|
||||
<select
|
||||
value={pane.reranker}
|
||||
onChange={(e) => updatePane(pane.id, { reranker: e.target.value })}
|
||||
className="px-2 py-1 border-2 border-border bg-background text-foreground rounded text-xs focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
>
|
||||
<option value="heuristic">Heuristic</option>
|
||||
<option value="cross-encoder">Cross-Encoder</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-bold mb-1 text-accent-foreground">Budget:</label>
|
||||
<input
|
||||
type="number"
|
||||
value={pane.thinkingBudget}
|
||||
onChange={(e) =>
|
||||
updatePane(pane.id, { thinkingBudget: parseInt(e.target.value) })
|
||||
}
|
||||
className="w-16 px-2 py-1 border-2 border-border bg-background text-foreground rounded text-xs focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-bold mb-1 text-accent-foreground">Max Tokens:</label>
|
||||
<input
|
||||
type="number"
|
||||
value={pane.maxTokens}
|
||||
onChange={(e) =>
|
||||
updatePane(pane.id, { maxTokens: parseInt(e.target.value) })
|
||||
}
|
||||
className="w-20 px-2 py-1 border-2 border-border bg-background text-foreground rounded text-xs focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => runSearch(pane.id)}
|
||||
disabled={pane.loading || !pane.query}
|
||||
className="px-4 py-1 bg-primary text-primary-foreground rounded font-bold text-xs hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{pane.loading ? 'Searching...' : '🔍 Search'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status Bar */}
|
||||
{pane.trace?.summary && (
|
||||
<div className="px-4 py-2 bg-secondary/20 border-b-2 border-primary text-xs flex gap-4 flex-wrap">
|
||||
<span className="text-secondary-foreground font-bold">✓ Search complete</span>
|
||||
<span className="text-muted-foreground">|</span>
|
||||
<span>
|
||||
<strong>Nodes visited:</strong> {pane.trace.summary.total_nodes_visited}
|
||||
</span>
|
||||
<span className="text-muted-foreground">|</span>
|
||||
<span>
|
||||
<strong>Entry points:</strong> {pane.trace.summary.entry_points_found}
|
||||
</span>
|
||||
<span className="text-muted-foreground">|</span>
|
||||
<span>
|
||||
<strong>Budget used:</strong> {pane.trace.summary.budget_used} /{' '}
|
||||
{pane.trace.summary.budget_used + pane.trace.summary.budget_remaining}
|
||||
</span>
|
||||
<span className="text-muted-foreground">|</span>
|
||||
<span>
|
||||
<strong>Results:</strong> {pane.trace.summary.results_returned}
|
||||
</span>
|
||||
<span className="text-muted-foreground">|</span>
|
||||
<span>
|
||||
<strong>Duration:</strong> {pane.trace.summary.total_duration_seconds?.toFixed(2)}
|
||||
s
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!pane.trace?.summary && !pane.loading && (
|
||||
<div className="px-4 py-2 bg-muted border-b-2 border-primary text-xs text-muted-foreground">
|
||||
Ready to search
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Phase Controls */}
|
||||
{pane.trace && (
|
||||
<div className="p-2.5 bg-card border-b-2 border-primary flex gap-3">
|
||||
<label className="flex items-center gap-1.5 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name={`phase-${pane.id}`}
|
||||
checked={pane.currentPhase === 'retrieval'}
|
||||
onChange={() => updatePane(pane.id, { currentPhase: 'retrieval' })}
|
||||
/>
|
||||
<span className="text-xs font-bold">1. Retrieval</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-1.5 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name={`phase-${pane.id}`}
|
||||
checked={pane.currentPhase === 'rrf'}
|
||||
onChange={() => updatePane(pane.id, { currentPhase: 'rrf' })}
|
||||
/>
|
||||
<span className="text-xs font-bold">2. RRF Merge</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-1.5 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name={`phase-${pane.id}`}
|
||||
checked={pane.currentPhase === 'rerank'}
|
||||
onChange={() => updatePane(pane.id, { currentPhase: 'rerank' })}
|
||||
/>
|
||||
<span className="text-xs font-bold">3. Reranking</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-1.5 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name={`phase-${pane.id}`}
|
||||
checked={pane.currentPhase === 'final'}
|
||||
onChange={() => updatePane(pane.id, { currentPhase: 'final' })}
|
||||
/>
|
||||
<span className="text-xs font-bold">4. Final Results</span>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<div className="bg-white overflow-auto" style={{ minHeight: '400px', maxHeight: '600px' }}>
|
||||
{pane.loading && (
|
||||
<div className="flex items-center justify-center h-96 text-gray-600">
|
||||
<div>
|
||||
<div className="text-4xl mb-2 text-center">🔄</div>
|
||||
<div className="text-sm">Searching...</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!pane.loading && !pane.trace && (
|
||||
<div className="flex items-center justify-center h-96 text-gray-400">
|
||||
<div className="text-center">
|
||||
<div className="text-4xl mb-2">🔍</div>
|
||||
<div className="text-sm">Enter a query and click Search</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!pane.loading && pane.trace && (
|
||||
<>
|
||||
{/* Retrieval Phase */}
|
||||
{pane.currentPhase === 'retrieval' && (
|
||||
<div>
|
||||
{/* Fact Type Tabs (only show if multiple fact types) */}
|
||||
{pane.factTypes.length > 1 && (
|
||||
<div className="flex gap-0 border-b-2 border-primary bg-accent">
|
||||
{pane.factTypes.map((ft) => (
|
||||
<button
|
||||
key={ft}
|
||||
onClick={() =>
|
||||
updatePane(pane.id, {
|
||||
currentRetrievalFactType: ft,
|
||||
})
|
||||
}
|
||||
className={`px-4 py-2 text-xs font-bold border-r border-border hover:opacity-80 ${
|
||||
pane.currentRetrievalFactType === ft
|
||||
? 'bg-secondary text-secondary-foreground'
|
||||
: 'bg-card text-card-foreground'
|
||||
}`}
|
||||
>
|
||||
{ft.charAt(0).toUpperCase() + ft.slice(1)} Facts
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{/* Retrieval Method Tabs */}
|
||||
<div className="flex gap-0 border-b-2 border-primary bg-muted">
|
||||
{['semantic', 'bm25', 'graph', 'temporal'].map((method) => (
|
||||
<button
|
||||
key={method}
|
||||
onClick={() =>
|
||||
updatePane(pane.id, {
|
||||
currentRetrievalMethod: method as RetrievalMethod,
|
||||
})
|
||||
}
|
||||
className={`px-4 py-2 text-xs font-bold border-r border-border hover:bg-accent ${
|
||||
pane.currentRetrievalMethod === method
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-background text-foreground'
|
||||
}`}
|
||||
>
|
||||
{method.charAt(0).toUpperCase() + method.slice(1)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{renderRetrievalResults(pane)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* RRF Merge Phase */}
|
||||
{pane.currentPhase === 'rrf' && renderRRFMerge(pane)}
|
||||
|
||||
{/* Reranking Phase */}
|
||||
{pane.currentPhase === 'rerank' && renderReranking(pane)}
|
||||
|
||||
{/* Final Results Phase */}
|
||||
{pane.currentPhase === 'final' && renderFinalResults(pane)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
196
control-plane/src/components/stats-view.tsx
Normal file
196
control-plane/src/components/stats-view.tsx
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { dataplaneClient } from '@/lib/api';
|
||||
import { useAgent } from '@/lib/agent-context';
|
||||
|
||||
export function StatsView() {
|
||||
const { currentAgent } = useAgent();
|
||||
const [stats, setStats] = useState<any>(null);
|
||||
const [operations, setOperations] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const loadStats = async () => {
|
||||
if (!currentAgent) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const [statsData, opsData]: [any, any] = await Promise.all([
|
||||
dataplaneClient.getAgentStats(currentAgent),
|
||||
fetch(`/api/operations/${currentAgent}`).then(r => r.json()),
|
||||
]);
|
||||
setStats(statsData);
|
||||
setOperations(opsData.operations || []);
|
||||
} catch (error) {
|
||||
console.error('Error loading stats:', error);
|
||||
alert('Error loading stats: ' + (error as Error).message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (currentAgent) {
|
||||
loadStats();
|
||||
// Refresh every 5 seconds
|
||||
const interval = setInterval(loadStats, 5000);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [currentAgent]);
|
||||
|
||||
if (!currentAgent) {
|
||||
return (
|
||||
<div className="p-10 text-center text-gray-600 bg-gray-50">
|
||||
<h3 className="text-xl font-semibold mb-2">No Agent Selected</h3>
|
||||
<p>Please select an agent from the dropdown above to view statistics.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading && !stats) {
|
||||
return (
|
||||
<div className="text-center py-10 text-gray-600">
|
||||
<div className="text-5xl mb-2.5">📊</div>
|
||||
<div className="text-lg">Loading statistics...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Stats Section */}
|
||||
{stats && (
|
||||
<div className="bg-white border-2 border-slate-800 rounded-lg p-5 mb-5 shadow">
|
||||
<h3 className="mt-0 mb-5 text-slate-800 text-lg font-bold flex items-center justify-between">
|
||||
<span>📊 Memory Statistics</span>
|
||||
<button
|
||||
onClick={loadStats}
|
||||
className="px-3 py-1 text-sm bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||
>
|
||||
🔄 Refresh
|
||||
</button>
|
||||
</h3>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="bg-gray-50 border-2 border-gray-300 rounded p-4 text-center transition-all hover:border-blue-400 hover:shadow">
|
||||
<div className="text-xs text-gray-600 font-semibold uppercase tracking-wide mb-2">Total Nodes</div>
|
||||
<div className="text-3xl font-bold text-slate-800">{stats.total_nodes || 0}</div>
|
||||
</div>
|
||||
<div className="bg-gray-50 border-2 border-gray-300 rounded p-4 text-center transition-all hover:border-blue-400 hover:shadow">
|
||||
<div className="text-xs text-gray-600 font-semibold uppercase tracking-wide mb-2">World Facts</div>
|
||||
<div className="text-3xl font-bold text-slate-800">{stats.nodes_by_type?.world || 0}</div>
|
||||
</div>
|
||||
<div className="bg-gray-50 border-2 border-gray-300 rounded p-4 text-center transition-all hover:border-blue-400 hover:shadow">
|
||||
<div className="text-xs text-gray-600 font-semibold uppercase tracking-wide mb-2">Agent Facts</div>
|
||||
<div className="text-3xl font-bold text-slate-800">{stats.nodes_by_type?.agent || 0}</div>
|
||||
</div>
|
||||
<div className="bg-gray-50 border-2 border-gray-300 rounded p-4 text-center transition-all hover:border-blue-400 hover:shadow">
|
||||
<div className="text-xs text-gray-600 font-semibold uppercase tracking-wide mb-2">Opinions</div>
|
||||
<div className="text-3xl font-bold text-slate-800">{stats.nodes_by_type?.opinion || 0}</div>
|
||||
</div>
|
||||
<div className="bg-gray-50 border-2 border-gray-300 rounded p-4 text-center transition-all hover:border-blue-400 hover:shadow">
|
||||
<div className="text-xs text-gray-600 font-semibold uppercase tracking-wide mb-2">Total Links</div>
|
||||
<div className="text-3xl font-bold text-slate-800">{stats.total_links || 0}</div>
|
||||
</div>
|
||||
<div className="bg-gray-50 border-2 border-gray-300 rounded p-4 text-center transition-all hover:border-blue-400 hover:shadow">
|
||||
<div className="text-xs text-gray-600 font-semibold uppercase tracking-wide mb-2">Temporal Links</div>
|
||||
<div className="text-3xl font-bold text-slate-800">{stats.links_by_type?.temporal || 0}</div>
|
||||
</div>
|
||||
<div className="bg-gray-50 border-2 border-gray-300 rounded p-4 text-center transition-all hover:border-blue-400 hover:shadow">
|
||||
<div className="text-xs text-gray-600 font-semibold uppercase tracking-wide mb-2">Semantic Links</div>
|
||||
<div className="text-3xl font-bold text-slate-800">{stats.links_by_type?.semantic || 0}</div>
|
||||
</div>
|
||||
<div className="bg-gray-50 border-2 border-gray-300 rounded p-4 text-center transition-all hover:border-blue-400 hover:shadow">
|
||||
<div className="text-xs text-gray-600 font-semibold uppercase tracking-wide mb-2">Entity Links</div>
|
||||
<div className="text-3xl font-bold text-slate-800">{stats.links_by_type?.entity || 0}</div>
|
||||
</div>
|
||||
<div className="bg-gray-50 border-2 border-gray-300 rounded p-4 text-center transition-all hover:border-blue-400 hover:shadow">
|
||||
<div className="text-xs text-gray-600 font-semibold uppercase tracking-wide mb-2">Documents</div>
|
||||
<div className="text-3xl font-bold text-slate-800">{stats.total_documents || 0}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Operations Section */}
|
||||
<div className="bg-white border-2 border-slate-800 rounded-lg p-5 shadow">
|
||||
<h3 className="mt-0 mb-5 text-slate-800 text-lg font-bold">⚙️ Async Operations</h3>
|
||||
|
||||
{stats && (stats.pending_operations > 0 || stats.failed_operations > 0) && (
|
||||
<div className="mb-4 p-3 bg-yellow-50 border border-yellow-300 rounded">
|
||||
<div className="flex gap-4">
|
||||
{stats.pending_operations > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-yellow-600 font-semibold">⏳ Pending:</span>
|
||||
<span className="text-yellow-800 font-bold">{stats.pending_operations}</span>
|
||||
</div>
|
||||
)}
|
||||
{stats.failed_operations > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-red-600 font-semibold">❌ Failed:</span>
|
||||
<span className="text-red-800 font-bold">{stats.failed_operations}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{operations.length > 0 ? (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full border-collapse text-xs">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="p-2.5 text-left border border-gray-300 bg-gray-100">ID</th>
|
||||
<th className="p-2.5 text-left border border-gray-300 bg-gray-100">Type</th>
|
||||
<th className="p-2.5 text-left border border-gray-300 bg-gray-100">Items</th>
|
||||
<th className="p-2.5 text-left border border-gray-300 bg-gray-100">Document ID</th>
|
||||
<th className="p-2.5 text-left border border-gray-300 bg-gray-100">Created</th>
|
||||
<th className="p-2.5 text-left border border-gray-300 bg-gray-100">Status</th>
|
||||
<th className="p-2.5 text-left border border-gray-300 bg-gray-100">Error</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{operations.map((op) => (
|
||||
<tr key={op.id} className={op.status === 'failed' ? 'bg-red-50' : ''}>
|
||||
<td className="p-2 border border-gray-300" title={op.id}>
|
||||
{op.id.substring(0, 8)}...
|
||||
</td>
|
||||
<td className="p-2 border border-gray-300">{op.task_type}</td>
|
||||
<td className="p-2 border border-gray-300">{op.items_count}</td>
|
||||
<td className="p-2 border border-gray-300">{op.document_id || 'N/A'}</td>
|
||||
<td className="p-2 border border-gray-300">
|
||||
{new Date(op.created_at).toLocaleString()}
|
||||
</td>
|
||||
<td className="p-2 border border-gray-300">
|
||||
<span
|
||||
className={`px-2 py-1 rounded text-xs font-bold ${
|
||||
op.status === 'pending'
|
||||
? 'bg-yellow-100 text-yellow-800'
|
||||
: op.status === 'failed'
|
||||
? 'bg-red-100 text-red-800'
|
||||
: 'bg-green-100 text-green-800'
|
||||
}`}
|
||||
>
|
||||
{op.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="p-2 border border-gray-300">
|
||||
{op.error_message ? (
|
||||
<span className="text-red-600" title={op.error_message}>
|
||||
{op.error_message.substring(0, 50)}...
|
||||
</span>
|
||||
) : (
|
||||
'None'
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-gray-600 text-center py-5">No operations found</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
162
control-plane/src/components/think-view.tsx
Normal file
162
control-plane/src/components/think-view.tsx
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { dataplaneClient } from '@/lib/api';
|
||||
import { useAgent } from '@/lib/agent-context';
|
||||
|
||||
export function ThinkView() {
|
||||
const { currentAgent } = useAgent();
|
||||
const [query, setQuery] = useState('');
|
||||
const [budget, setBudget] = useState(50);
|
||||
const [result, setResult] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const runThink = async () => {
|
||||
if (!currentAgent || !query) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const data: any = await dataplaneClient.think({
|
||||
query,
|
||||
agent_id: currentAgent,
|
||||
thinking_budget: budget,
|
||||
});
|
||||
setResult(data);
|
||||
} catch (error) {
|
||||
console.error('Error running think:', error);
|
||||
alert('Error running think: ' + (error as Error).message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl">
|
||||
<p className="text-gray-600 mb-4">
|
||||
Ask questions and get AI-generated answers based on agent identity and world facts.
|
||||
</p>
|
||||
|
||||
<div className="bg-gray-50 p-5 rounded-lg border-2 border-slate-800 mb-5">
|
||||
<div className="flex gap-4 items-end flex-wrap mb-4">
|
||||
<div className="flex-1 min-w-[300px]">
|
||||
<label className="font-bold block mb-1">Question:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Enter your question..."
|
||||
className="w-full px-2.5 py-2 border-2 border-gray-300 rounded text-sm"
|
||||
onKeyDown={(e) => e.key === 'Enter' && runThink()}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="font-bold block mb-1">Budget:</label>
|
||||
<input
|
||||
type="number"
|
||||
value={budget}
|
||||
onChange={(e) => setBudget(parseInt(e.target.value))}
|
||||
min="10"
|
||||
max="1000"
|
||||
className="w-20 px-2.5 py-2 border-2 border-gray-300 rounded text-sm"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={runThink}
|
||||
disabled={loading || !query}
|
||||
className="px-6 py-2 bg-green-500 text-white rounded cursor-pointer font-bold text-sm hover:bg-green-600 disabled:bg-gray-400"
|
||||
>
|
||||
💭 Think
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading && (
|
||||
<div className="text-center py-10 text-gray-600">
|
||||
<div className="text-5xl mb-2.5">💭</div>
|
||||
<div className="text-lg">Thinking...</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{result && !loading && (
|
||||
<div>
|
||||
<div className="bg-white p-5 rounded-lg border-2 border-slate-800 shadow mb-8">
|
||||
<h3 className="mt-0 text-slate-800 border-b-2 border-slate-800 pb-2.5">Answer</h3>
|
||||
<div className="p-4 bg-gray-50 border-l-4 border-green-500 text-base leading-relaxed whitespace-pre-wrap">
|
||||
{result.text}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-5 rounded-lg border-2 border-slate-800 shadow">
|
||||
<h3 className="mt-0 text-slate-800 border-b-2 border-slate-800 pb-2.5">Based On</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-5 mt-4">
|
||||
<div>
|
||||
<h4 className="mt-0 mb-2.5 text-blue-700">World Facts (General Knowledge)</h4>
|
||||
<div className="bg-blue-50 p-4 rounded border-2 border-blue-700 min-h-[100px]">
|
||||
{result.based_on?.world?.length > 0 ? (
|
||||
<ul className="text-sm">
|
||||
{result.based_on.world.map((fact: any, i: number) => (
|
||||
<li key={i} className="mb-2">
|
||||
{fact.text} <span className="text-gray-500">({fact.score?.toFixed(2)})</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="text-gray-500 text-sm">None</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="mt-0 mb-2.5 text-orange-700">Agent Facts (Identity)</h4>
|
||||
<div className="bg-orange-50 p-4 rounded border-2 border-orange-700 min-h-[100px]">
|
||||
{result.based_on?.agent?.length > 0 ? (
|
||||
<ul className="text-sm">
|
||||
{result.based_on.agent.map((fact: any, i: number) => (
|
||||
<li key={i} className="mb-2">
|
||||
{fact.text} <span className="text-gray-500">({fact.score?.toFixed(2)})</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="text-gray-500 text-sm">None</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="mt-0 mb-2.5 text-purple-700">Opinions (Agent Beliefs)</h4>
|
||||
<div className="bg-purple-50 p-4 rounded border-2 border-purple-700 min-h-[100px]">
|
||||
{result.based_on?.opinion?.length > 0 ? (
|
||||
<ul className="text-sm">
|
||||
{result.based_on.opinion.map((fact: any, i: number) => (
|
||||
<li key={i} className="mb-2">
|
||||
{fact.text} <span className="text-gray-500">({fact.score?.toFixed(2)})</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="text-gray-500 text-sm">None</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{result.new_opinions && result.new_opinions.length > 0 && (
|
||||
<div className="mt-8 bg-green-50 p-5 rounded-lg border-2 border-green-500">
|
||||
<h3 className="mt-0 text-green-800 border-b-2 border-green-500 pb-2.5">✨ New Opinions Formed</h3>
|
||||
<div className="mt-4">
|
||||
{result.new_opinions.map((opinion: any, i: number) => (
|
||||
<div key={i} className="mb-2 p-3 bg-white rounded border border-green-300">
|
||||
<div className="font-semibold">{opinion.text}</div>
|
||||
<div className="text-sm text-gray-600">Confidence: {opinion.confidence?.toFixed(2)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
45
control-plane/src/lib/agent-context.tsx
Normal file
45
control-plane/src/lib/agent-context.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
'use client';
|
||||
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
import { dataplaneClient } from './api';
|
||||
|
||||
interface AgentContextType {
|
||||
currentAgent: string | null;
|
||||
setCurrentAgent: (agent: string | null) => void;
|
||||
agents: string[];
|
||||
loadAgents: () => Promise<void>;
|
||||
}
|
||||
|
||||
const AgentContext = createContext<AgentContextType | undefined>(undefined);
|
||||
|
||||
export function AgentProvider({ children }: { children: React.ReactNode }) {
|
||||
const [currentAgent, setCurrentAgent] = useState<string | null>(null);
|
||||
const [agents, setAgents] = useState<string[]>([]);
|
||||
|
||||
const loadAgents = async () => {
|
||||
try {
|
||||
const data = await dataplaneClient.listAgents();
|
||||
setAgents(data.agents);
|
||||
} catch (error) {
|
||||
console.error('Error loading agents:', error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadAgents();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AgentContext.Provider value={{ currentAgent, setCurrentAgent, agents, loadAgents }}>
|
||||
{children}
|
||||
</AgentContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useAgent() {
|
||||
const context = useContext(AgentContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useAgent must be used within an AgentProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
242
control-plane/src/lib/api.ts
Normal file
242
control-plane/src/lib/api.ts
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
/**
|
||||
* API client for the control plane API (which proxies to the dataplane)
|
||||
*/
|
||||
|
||||
export class DataplaneClient {
|
||||
private async fetchApi<T>(
|
||||
path: string,
|
||||
options?: RequestInit
|
||||
): Promise<T> {
|
||||
// Call the control plane API routes, not the dataplane directly
|
||||
const response = await fetch(path, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options?.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`API Error: ${response.status} - ${error}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Search memory using semantic similarity
|
||||
*/
|
||||
async search(params: {
|
||||
query: string;
|
||||
fact_type: ('world' | 'agent' | 'opinion')[];
|
||||
agent_id?: string;
|
||||
thinking_budget?: number;
|
||||
max_tokens?: number;
|
||||
reranker?: string;
|
||||
trace?: boolean;
|
||||
}) {
|
||||
return this.fetchApi('/api/search', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
agent_id: params.agent_id || 'default',
|
||||
...params,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Think and generate answer
|
||||
*/
|
||||
async think(params: {
|
||||
query: string;
|
||||
agent_id?: string;
|
||||
thinking_budget?: number;
|
||||
}) {
|
||||
return this.fetchApi('/api/think', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
agent_id: params.agent_id || 'default',
|
||||
...params,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Store multiple memories in batch
|
||||
*/
|
||||
async batchPut(params: {
|
||||
agent_id: string;
|
||||
items: Array<{
|
||||
content: string;
|
||||
event_date?: string;
|
||||
context?: string;
|
||||
}>;
|
||||
document_id?: string;
|
||||
}) {
|
||||
return this.fetchApi('/api/memories/batch', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Store multiple memories asynchronously
|
||||
* Note: If document_id is provided and already exists, the document will be automatically replaced (upsert behavior).
|
||||
*/
|
||||
async batchPutAsync(params: {
|
||||
agent_id: string;
|
||||
items: Array<{
|
||||
content: string;
|
||||
event_date?: string;
|
||||
context?: string;
|
||||
}>;
|
||||
document_id?: string;
|
||||
}) {
|
||||
return this.fetchApi('/api/memories/batch_async', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* List all agents
|
||||
*/
|
||||
async listAgents() {
|
||||
return this.fetchApi<{ agents: string[] }>('/api/agents', { cache: 'no-store' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get agent statistics
|
||||
*/
|
||||
async getAgentStats(agentId: string) {
|
||||
return this.fetchApi(`/api/stats/${agentId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get graph data for visualization
|
||||
*/
|
||||
async getGraphData(params?: {
|
||||
agent_id?: string;
|
||||
fact_type?: string;
|
||||
}) {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (params?.agent_id) queryParams.append('agent_id', params.agent_id);
|
||||
if (params?.fact_type) queryParams.append('fact_type', params.fact_type);
|
||||
|
||||
const path = `/api/graph${queryParams.toString() ? `?${queryParams}` : ''}`;
|
||||
return this.fetchApi(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* List memory units
|
||||
*/
|
||||
async listMemoryUnits(params?: {
|
||||
agent_id?: string;
|
||||
fact_type?: string;
|
||||
q?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}) {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (params?.agent_id) queryParams.append('agent_id', params.agent_id);
|
||||
if (params?.fact_type) queryParams.append('fact_type', params.fact_type);
|
||||
if (params?.q) queryParams.append('q', params.q);
|
||||
if (params?.limit) queryParams.append('limit', params.limit.toString());
|
||||
if (params?.offset) queryParams.append('offset', params.offset.toString());
|
||||
|
||||
const path = `/api/list${queryParams.toString() ? `?${queryParams}` : ''}`;
|
||||
return this.fetchApi(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* List documents
|
||||
*/
|
||||
async listDocuments(params?: {
|
||||
agent_id?: string;
|
||||
q?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}) {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (params?.agent_id) queryParams.append('agent_id', params.agent_id);
|
||||
if (params?.q) queryParams.append('q', params.q);
|
||||
if (params?.limit) queryParams.append('limit', params.limit.toString());
|
||||
if (params?.offset) queryParams.append('offset', params.offset.toString());
|
||||
|
||||
const path = `/api/documents${queryParams.toString() ? `?${queryParams}` : ''}`;
|
||||
return this.fetchApi(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get document by ID
|
||||
*/
|
||||
async getDocument(documentId: string, agentId: string) {
|
||||
const queryParams = new URLSearchParams({ agent_id: agentId });
|
||||
return this.fetchApi(`/api/documents/${documentId}?${queryParams}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* List async operations for an agent
|
||||
*/
|
||||
async listOperations(agentId: string) {
|
||||
return this.fetchApi(`/api/operations/${agentId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel a pending async operation
|
||||
*/
|
||||
async cancelOperation(operationId: string) {
|
||||
return this.fetchApi(`/api/operations/${operationId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a memory unit
|
||||
*/
|
||||
async deleteMemoryUnit(unitId: string) {
|
||||
return this.fetchApi(`/api/memory/${unitId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Export a singleton instance
|
||||
export const dataplaneClient = new DataplaneClient();
|
||||
|
||||
/**
|
||||
* Server-side dataplane client that calls the dataplane directly
|
||||
* Only use this on the server side (in API routes)
|
||||
*/
|
||||
export class ServerDataplaneClient {
|
||||
private baseUrl: string;
|
||||
|
||||
constructor() {
|
||||
this.baseUrl = process.env.NEXT_PUBLIC_DATAPLANE_API_URL || 'http://localhost:8080';
|
||||
}
|
||||
|
||||
async fetchDataplane<T>(
|
||||
path: string,
|
||||
options?: RequestInit
|
||||
): Promise<T> {
|
||||
const url = `${this.baseUrl}${path}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options?.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`Dataplane Error: ${response.status} - ${error}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
}
|
||||
|
||||
export const serverDataplaneClient = new ServerDataplaneClient();
|
||||
6
control-plane/src/lib/utils.ts
Normal file
6
control-plane/src/lib/utils.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
38
control-plane/tailwind.config.ts
Normal file
38
control-plane/tailwind.config.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import type { Config } from "tailwindcss";
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
41
control-plane/tsconfig.json
Normal file
41
control-plane/tsconfig.json
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
},
|
||||
"target": "ES2017"
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ the FastAPI application with all API endpoints.
|
|||
import logging
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Optional, List, Dict, Any
|
||||
from typing import Optional, List, Dict, Any, Union
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import FastAPI, HTTPException
|
||||
|
|
@ -21,23 +21,25 @@ from memora import TemporalSemanticMemory
|
|||
class SearchRequest(BaseModel):
|
||||
"""Request model for search endpoint."""
|
||||
query: str
|
||||
fact_type: str
|
||||
fact_type: List[str] # List of fact types to search
|
||||
agent_id: str = "default"
|
||||
thinking_budget: int = 100
|
||||
max_tokens: int = 4096
|
||||
reranker: str = "heuristic"
|
||||
trace: bool = False
|
||||
question_date: Optional[str] = None # ISO format date string (e.g., "2023-05-30T23:40:00")
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"query": "What did Alice say about machine learning?",
|
||||
"fact_type": "world",
|
||||
"fact_type": ["world", "agent"],
|
||||
"agent_id": "user123",
|
||||
"thinking_budget": 100,
|
||||
"max_tokens": 4096,
|
||||
"reranker": "heuristic",
|
||||
"trace": True
|
||||
"trace": True,
|
||||
"question_date": "2023-05-30T23:40:00"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -87,8 +89,6 @@ class BatchPutRequest(BaseModel):
|
|||
agent_id: str
|
||||
items: List[MemoryItem]
|
||||
document_id: Optional[str] = None
|
||||
document_metadata: Optional[Dict[str, Any]] = None
|
||||
upsert: bool = False
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
|
|
@ -104,8 +104,7 @@ class BatchPutRequest(BaseModel):
|
|||
"event_date": "2024-01-15T10:00:00Z"
|
||||
}
|
||||
],
|
||||
"document_id": "conversation_123",
|
||||
"upsert": False
|
||||
"document_id": "conversation_123"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -357,10 +356,6 @@ The system uses:
|
|||
}
|
||||
)
|
||||
|
||||
# Mount static files (web directory is sibling to this file)
|
||||
web_dir = Path(__file__).parent / "web"
|
||||
app.mount("/static", StaticFiles(directory=str(web_dir / "static")), name="static")
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
"""Initialize memory system on startup."""
|
||||
|
|
@ -387,9 +382,12 @@ def _register_routes(app: FastAPI):
|
|||
|
||||
@app.get("/", include_in_schema=False)
|
||||
async def index():
|
||||
"""Serve the visualization page."""
|
||||
web_dir = Path(__file__).parent / "web"
|
||||
return FileResponse(str(web_dir / "templates" / "index.html"))
|
||||
"""Root endpoint - directs to control plane."""
|
||||
return {
|
||||
"message": "Memory Control Plane API",
|
||||
"docs": "/docs",
|
||||
"control_plane": "The web UI has moved to the Next.js control plane. Please use the control-plane directory."
|
||||
}
|
||||
|
||||
|
||||
@app.get(
|
||||
|
|
@ -471,14 +469,33 @@ def _register_routes(app: FastAPI):
|
|||
async def api_search(request: SearchRequest):
|
||||
"""Run a search and return results with trace."""
|
||||
try:
|
||||
# Validate fact_type
|
||||
# Validate fact_type(s)
|
||||
valid_fact_types = ["world", "agent", "opinion"]
|
||||
if request.fact_type not in valid_fact_types:
|
||||
|
||||
if not request.fact_type:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Invalid fact_type '{request.fact_type}'. Must be one of: {', '.join(valid_fact_types)}"
|
||||
detail="fact_type must be a non-empty list"
|
||||
)
|
||||
|
||||
for ft in request.fact_type:
|
||||
if ft not in valid_fact_types:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Invalid fact_type '{ft}'. Must be one of: {', '.join(valid_fact_types)}"
|
||||
)
|
||||
|
||||
# Parse question_date if provided
|
||||
question_date = None
|
||||
if request.question_date:
|
||||
try:
|
||||
question_date = datetime.fromisoformat(request.question_date.replace('Z', '+00:00'))
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Invalid question_date format. Expected ISO format (e.g., '2023-05-30T23:40:00'): {str(e)}"
|
||||
)
|
||||
|
||||
# Run search with tracing
|
||||
results, trace = await app.state.memory.search_async(
|
||||
agent_id=request.agent_id,
|
||||
|
|
@ -487,7 +504,8 @@ def _register_routes(app: FastAPI):
|
|||
max_tokens=request.max_tokens,
|
||||
enable_trace=request.trace,
|
||||
reranker=request.reranker,
|
||||
fact_type=request.fact_type
|
||||
fact_type=request.fact_type,
|
||||
question_date=question_date
|
||||
)
|
||||
|
||||
# Convert trace to dict
|
||||
|
|
@ -727,7 +745,7 @@ def _register_routes(app: FastAPI):
|
|||
- Efficient batch processing
|
||||
- Automatic fact extraction from natural language
|
||||
- Entity recognition and linking
|
||||
- Document tracking with optional upsert
|
||||
- Document tracking with automatic upsert (when document_id is provided)
|
||||
- Temporal and semantic linking
|
||||
|
||||
The system automatically:
|
||||
|
|
@ -736,6 +754,8 @@ def _register_routes(app: FastAPI):
|
|||
3. Deduplicates similar facts
|
||||
4. Creates temporal, semantic, and entity links
|
||||
5. Tracks document metadata
|
||||
|
||||
Note: If document_id is provided and already exists, the old document and its memory units will be deleted before creating new ones (upsert behavior).
|
||||
"""
|
||||
)
|
||||
async def api_batch_put(request: BatchPutRequest):
|
||||
|
|
@ -762,9 +782,7 @@ def _register_routes(app: FastAPI):
|
|||
result = await app.state.memory.put_batch_async(
|
||||
agent_id=request.agent_id,
|
||||
contents=contents,
|
||||
document_id=request.document_id,
|
||||
document_metadata=request.document_metadata,
|
||||
upsert=request.upsert
|
||||
document_id=request.document_id
|
||||
)
|
||||
logging.info(f"Batch put result: {result}")
|
||||
|
||||
|
|
@ -799,13 +817,15 @@ def _register_routes(app: FastAPI):
|
|||
- Efficient batch processing
|
||||
- Automatic fact extraction from natural language
|
||||
- Entity recognition and linking
|
||||
- Document tracking with optional upsert
|
||||
- Document tracking with automatic upsert (when document_id is provided)
|
||||
- Temporal and semantic linking
|
||||
|
||||
The system automatically:
|
||||
1. Queues the batch put task
|
||||
2. Returns immediately with success=True, queued=True
|
||||
3. Processes in background: extracts facts, generates embeddings, creates links
|
||||
|
||||
Note: If document_id is provided and already exists, the old document and its memory units will be deleted before creating new ones (upsert behavior).
|
||||
"""
|
||||
)
|
||||
async def api_batch_put_async(request: BatchPutRequest):
|
||||
|
|
@ -852,9 +872,7 @@ def _register_routes(app: FastAPI):
|
|||
'operation_id': str(operation_id),
|
||||
'agent_id': request.agent_id,
|
||||
'contents': contents,
|
||||
'document_id': request.document_id,
|
||||
'document_metadata': request.document_metadata,
|
||||
'upsert': request.upsert
|
||||
'document_id': request.document_id
|
||||
})
|
||||
|
||||
logging.info(f"Batch put task queued for agent_id={request.agent_id}, {len(contents)} items, operation_id={operation_id}")
|
||||
|
|
|
|||
|
|
@ -65,47 +65,36 @@ class EntityResolver:
|
|||
import time
|
||||
start = time.time()
|
||||
|
||||
# Group entities by type for efficient querying
|
||||
entities_by_type = {}
|
||||
for idx, entity_data in enumerate(entities_data):
|
||||
entity_type = entity_data['type']
|
||||
if entity_type not in entities_by_type:
|
||||
entities_by_type[entity_type] = []
|
||||
entities_by_type[entity_type].append((idx, entity_data))
|
||||
# Query ALL candidates for this agent
|
||||
all_entities = await conn.fetch(
|
||||
"""
|
||||
SELECT canonical_name, id, metadata, last_seen, mention_count
|
||||
FROM entities
|
||||
WHERE agent_id = $1
|
||||
""",
|
||||
agent_id
|
||||
)
|
||||
|
||||
# Query ALL candidates for each type in batch
|
||||
all_candidates = {} # Maps (entity_type, entity_text) -> list of candidates
|
||||
for entity_type, entities_list in entities_by_type.items():
|
||||
# Extract unique entity texts for this type
|
||||
entity_texts = list(set(e[1]['text'] for e in entities_list))
|
||||
# Build candidate map for each entity text
|
||||
all_candidates = {} # Maps entity_text -> list of candidates
|
||||
entity_texts = list(set(e['text'] for e in entities_data))
|
||||
|
||||
# Query candidates for all texts at once
|
||||
type_candidates = await conn.fetch(
|
||||
"""
|
||||
SELECT canonical_name, id, metadata, last_seen, mention_count
|
||||
FROM entities
|
||||
WHERE agent_id = $1 AND entity_type = $2
|
||||
""",
|
||||
agent_id, entity_type
|
||||
)
|
||||
|
||||
# Filter candidates in memory (faster than complex SQL for small datasets)
|
||||
for entity_text in entity_texts:
|
||||
matching = []
|
||||
entity_text_lower = entity_text.lower()
|
||||
for row in type_candidates:
|
||||
canonical_name = row['canonical_name']
|
||||
ent_id = row['id']
|
||||
metadata = row['metadata']
|
||||
last_seen = row['last_seen']
|
||||
mention_count = row['mention_count']
|
||||
canonical_lower = canonical_name.lower()
|
||||
# Same matching logic as before
|
||||
if (entity_text_lower == canonical_lower or
|
||||
entity_text_lower in canonical_lower or
|
||||
canonical_lower in entity_text_lower):
|
||||
matching.append((ent_id, canonical_name, metadata, last_seen, mention_count))
|
||||
all_candidates[(entity_type, entity_text)] = matching
|
||||
for entity_text in entity_texts:
|
||||
matching = []
|
||||
entity_text_lower = entity_text.lower()
|
||||
for row in all_entities:
|
||||
canonical_name = row['canonical_name']
|
||||
ent_id = row['id']
|
||||
metadata = row['metadata']
|
||||
last_seen = row['last_seen']
|
||||
mention_count = row['mention_count']
|
||||
canonical_lower = canonical_name.lower()
|
||||
# Match if exact or substring match
|
||||
if (entity_text_lower == canonical_lower or
|
||||
entity_text_lower in canonical_lower or
|
||||
canonical_lower in entity_text_lower):
|
||||
matching.append((ent_id, canonical_name, metadata, last_seen, mention_count))
|
||||
all_candidates[entity_text] = matching
|
||||
|
||||
# Resolve each entity using pre-fetched candidates
|
||||
entity_ids = [None] * len(entities_data)
|
||||
|
|
@ -114,10 +103,9 @@ class EntityResolver:
|
|||
|
||||
for idx, entity_data in enumerate(entities_data):
|
||||
entity_text = entity_data['text']
|
||||
entity_type = entity_data['type']
|
||||
nearby_entities = entity_data.get('nearby_entities', [])
|
||||
|
||||
candidates = all_candidates.get((entity_type, entity_text), [])
|
||||
candidates = all_candidates.get(entity_text, [])
|
||||
|
||||
if not candidates:
|
||||
# Will create new entity
|
||||
|
|
@ -154,8 +142,8 @@ class EntityResolver:
|
|||
best_candidate = candidate_id
|
||||
best_name_similarity = name_similarity
|
||||
|
||||
# Apply threshold
|
||||
threshold = 0.4 if entity_type == 'PERSON' and best_name_similarity >= 0.95 else 0.6
|
||||
# Apply unified threshold
|
||||
threshold = 0.6
|
||||
|
||||
if best_score > threshold:
|
||||
entity_ids[idx] = best_candidate
|
||||
|
|
@ -185,20 +173,19 @@ class EntityResolver:
|
|||
param_idx = 1
|
||||
|
||||
for idx, entity_data in entities_to_create:
|
||||
values_clauses.append(f"(${param_idx}, ${param_idx+1}, ${param_idx+2}, ${param_idx+3}, ${param_idx+4}, ${param_idx+5})")
|
||||
values_clauses.append(f"(${param_idx}, ${param_idx+1}, ${param_idx+2}, ${param_idx+3}, ${param_idx+4})")
|
||||
params.extend([
|
||||
agent_id,
|
||||
entity_data['text'],
|
||||
entity_data['type'],
|
||||
unit_event_date,
|
||||
unit_event_date,
|
||||
1
|
||||
])
|
||||
param_idx += 6
|
||||
param_idx += 5
|
||||
|
||||
# Single INSERT with multiple VALUES rows
|
||||
query = f"""
|
||||
INSERT INTO entities (agent_id, canonical_name, entity_type, first_seen, last_seen, mention_count)
|
||||
INSERT INTO entities (agent_id, canonical_name, first_seen, last_seen, mention_count)
|
||||
VALUES {', '.join(values_clauses)}
|
||||
RETURNING id
|
||||
"""
|
||||
|
|
@ -216,7 +203,6 @@ class EntityResolver:
|
|||
self,
|
||||
agent_id: str,
|
||||
entity_text: str,
|
||||
entity_type: str,
|
||||
context: str,
|
||||
nearby_entities: List[Dict],
|
||||
unit_event_date,
|
||||
|
|
@ -227,7 +213,6 @@ class EntityResolver:
|
|||
Args:
|
||||
agent_id: Agent ID (entities are scoped to agents)
|
||||
entity_text: Entity text ("Alice", "Google", etc.)
|
||||
entity_type: Entity type (PERSON, ORG, etc.)
|
||||
context: Context where entity appears
|
||||
nearby_entities: Other entities in the same unit
|
||||
unit_event_date: When this unit was created
|
||||
|
|
@ -236,27 +221,26 @@ class EntityResolver:
|
|||
Entity ID (creates new entity if needed)
|
||||
"""
|
||||
async with self.pool.acquire() as conn:
|
||||
# Find candidate entities with same type and similar name
|
||||
# Find candidate entities with similar name
|
||||
candidates = await conn.fetch(
|
||||
"""
|
||||
SELECT id, canonical_name, metadata, last_seen
|
||||
FROM entities
|
||||
WHERE agent_id = $1
|
||||
AND entity_type = $2
|
||||
AND (
|
||||
canonical_name ILIKE $3
|
||||
OR canonical_name ILIKE $4
|
||||
OR $3 ILIKE canonical_name || '%%'
|
||||
canonical_name ILIKE $2
|
||||
OR canonical_name ILIKE $3
|
||||
OR $2 ILIKE canonical_name || '%%'
|
||||
)
|
||||
ORDER BY mention_count DESC
|
||||
""",
|
||||
agent_id, entity_type, entity_text, f"%{entity_text}%"
|
||||
agent_id, entity_text, f"%{entity_text}%"
|
||||
)
|
||||
|
||||
if not candidates:
|
||||
# New entity - create it
|
||||
return await self._create_entity(
|
||||
conn, agent_id, entity_text, entity_type, unit_event_date
|
||||
conn, agent_id, entity_text, unit_event_date
|
||||
)
|
||||
|
||||
# Score candidates based on:
|
||||
|
|
@ -324,8 +308,7 @@ class EntityResolver:
|
|||
best_name_similarity = name_similarity
|
||||
|
||||
# Threshold for considering it the same entity
|
||||
# For PERSON entities with exact name match, use lower threshold
|
||||
threshold = 0.4 if entity_type == 'PERSON' and best_name_similarity >= 0.95 else 0.6
|
||||
threshold = 0.6
|
||||
|
||||
if best_score > threshold:
|
||||
# Update entity
|
||||
|
|
@ -342,7 +325,7 @@ class EntityResolver:
|
|||
else:
|
||||
# Not confident - create new entity
|
||||
return await self._create_entity(
|
||||
conn, agent_id, entity_text, entity_type, unit_event_date
|
||||
conn, agent_id, entity_text, unit_event_date
|
||||
)
|
||||
|
||||
async def _create_entity(
|
||||
|
|
@ -350,7 +333,6 @@ class EntityResolver:
|
|||
conn,
|
||||
agent_id: str,
|
||||
entity_text: str,
|
||||
entity_type: str,
|
||||
event_date,
|
||||
) -> str:
|
||||
"""
|
||||
|
|
@ -360,7 +342,6 @@ class EntityResolver:
|
|||
conn: Database connection
|
||||
agent_id: Agent ID
|
||||
entity_text: Entity text
|
||||
entity_type: Entity type
|
||||
event_date: When first seen
|
||||
|
||||
Returns:
|
||||
|
|
@ -368,11 +349,11 @@ class EntityResolver:
|
|||
"""
|
||||
entity_id = await conn.fetchval(
|
||||
"""
|
||||
INSERT INTO entities (agent_id, canonical_name, entity_type, first_seen, last_seen, mention_count)
|
||||
VALUES ($1, $2, $3, $4, $5, 1)
|
||||
INSERT INTO entities (agent_id, canonical_name, first_seen, last_seen, mention_count)
|
||||
VALUES ($1, $2, $3, $4, 1)
|
||||
RETURNING id
|
||||
""",
|
||||
agent_id, entity_text, entity_type, event_date, event_date
|
||||
agent_id, entity_text, event_date, event_date
|
||||
)
|
||||
return entity_id
|
||||
|
||||
|
|
@ -535,7 +516,6 @@ class EntityResolver:
|
|||
self,
|
||||
agent_id: str,
|
||||
entity_text: str,
|
||||
entity_type: Optional[str] = None
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Find an entity by text (for query resolution).
|
||||
|
|
@ -543,34 +523,20 @@ class EntityResolver:
|
|||
Args:
|
||||
agent_id: Agent ID
|
||||
entity_text: Entity text to search for
|
||||
entity_type: Optional entity type filter
|
||||
|
||||
Returns:
|
||||
Entity ID if found, None otherwise
|
||||
"""
|
||||
async with self.pool.acquire() as conn:
|
||||
if entity_type:
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
SELECT id FROM entities
|
||||
WHERE agent_id = $1
|
||||
AND entity_type = $2
|
||||
AND canonical_name ILIKE $3
|
||||
ORDER BY mention_count DESC
|
||||
LIMIT 1
|
||||
""",
|
||||
agent_id, entity_type, entity_text
|
||||
)
|
||||
else:
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
SELECT id FROM entities
|
||||
WHERE agent_id = $1
|
||||
AND canonical_name ILIKE $2
|
||||
ORDER BY mention_count DESC
|
||||
LIMIT 1
|
||||
""",
|
||||
agent_id, entity_text
|
||||
)
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
SELECT id FROM entities
|
||||
WHERE agent_id = $1
|
||||
AND canonical_name ILIKE $2
|
||||
ORDER BY mention_count DESC
|
||||
LIMIT 1
|
||||
""",
|
||||
agent_id, entity_text
|
||||
)
|
||||
|
||||
return row['id'] if row else None
|
||||
|
|
|
|||
|
|
@ -20,9 +20,6 @@ class Entity(BaseModel):
|
|||
text: str = Field(
|
||||
description="The entity name as it appears in the fact"
|
||||
)
|
||||
type: Literal["PERSON", "ORG", "PLACE", "PRODUCT", "CONCEPT", "OTHER"] = Field(
|
||||
description="Entity type: PERSON, ORG, PLACE, PRODUCT, CONCEPT, or OTHER for entities that don't fit other categories"
|
||||
)
|
||||
|
||||
|
||||
class ExtractedFact(BaseModel):
|
||||
|
|
@ -184,13 +181,7 @@ Classify each fact as either 'world' or 'agent':
|
|||
- **'agent'**: Only for AI agent's own actions
|
||||
|
||||
## ENTITY EXTRACTION
|
||||
Extract ALL important entities with types:
|
||||
- **PERSON**: Names of individuals
|
||||
- **ORG**: Companies, institutions, teams
|
||||
- **PLACE**: Cities, countries, locations
|
||||
- **PRODUCT**: Products, tools, technologies
|
||||
- **CONCEPT**: Topics, projects, subjects
|
||||
- **OTHER**: Entities that don't fit above
|
||||
Extract ALL important entities (names of people, places, organizations, products, concepts, etc).
|
||||
|
||||
Extract proper nouns and key identifying terms. Skip pronouns and generic terms.
|
||||
|
||||
|
|
@ -217,7 +208,7 @@ Bob: Perfect, let's go with that!"
|
|||
**✅ GOOD (one comprehensive fact):**
|
||||
"Alice and Bob discussed naming their summer party playlist. Bob suggested 'Summer Vibes' because it's catchy and seasonal, but Alice wanted something more unique. Bob then proposed 'Sunset Sessions' and 'Beach Beats', with Alice favoring 'Beach Beats' for its playful and fun tone. They ultimately decided on 'Beach Beats' as the final name."
|
||||
- fact_type: "world"
|
||||
- entities: [{{"text": "Alice", "type": "PERSON"}}, {{"text": "Bob", "type": "PERSON"}}]
|
||||
- entities: [{{"text": "Alice"}}, {{"text": "Bob"}}]
|
||||
|
||||
### Example 2: Photo Sharing with Context
|
||||
**Input:**
|
||||
|
|
@ -231,7 +222,7 @@ Nate: I picked bright orange because it's bold and makes me feel confident. Plus
|
|||
**✅ GOOD (comprehensive with all context):**
|
||||
"Nate shared a photo of his new bright orange hair. When asked why he chose that color, Nate explained he picked it because it's bold and makes him feel confident, and it matches his personality."
|
||||
- fact_type: "world"
|
||||
- entities: [{{"text": "Nate", "type": "PERSON"}}]
|
||||
- entities: [{{"text": "Nate"}}]
|
||||
- NOTE: Preserves that it's a PHOTO, it's NEW hair, the COLOR, and the FULL reasoning
|
||||
|
||||
### Example 3: Travel Planning
|
||||
|
|
@ -252,7 +243,7 @@ Sarah: Sounds amazing! I'll add it to my itinerary."
|
|||
"Sarah is planning to visit Japan next spring, and Mike recommended Kyoto as the perfect destination for cherry blossom season. Mike explained that Kyoto has the most beautiful temples and spectacular cherry blossoms, based on his visit there in 2019. Sarah decided to add Kyoto to her itinerary."
|
||||
- fact_type: "world"
|
||||
- date: Next spring from reference date
|
||||
- entities: [{{"text": "Sarah", "type": "PERSON"}}, {{"text": "Mike", "type": "PERSON"}}, {{"text": "Japan", "type": "PLACE"}}, {{"text": "Kyoto", "type": "PLACE"}}]
|
||||
- entities: [{{"text": "Sarah"}}, {{"text": "Mike"}}, {{"text": "Japan"}}, {{"text": "Kyoto"}}]
|
||||
|
||||
### Example 4: Job News
|
||||
**Input:**
|
||||
|
|
@ -262,7 +253,7 @@ Sarah: Sounds amazing! I'll add it to my itinerary."
|
|||
"Alice works at Google in Mountain View on the AI team, which she joined in 2023, and she loves the company culture there."
|
||||
- fact_type: "world"
|
||||
- date: 2023 (if reference is 2024)
|
||||
- entities: [{{"text": "Alice", "type": "PERSON"}}, {{"text": "Google", "type": "ORG"}}, {{"text": "Mountain View", "type": "PLACE"}}, {{"text": "AI team", "type": "ORG"}}]
|
||||
- entities: [{{"text": "Alice"}}, {{"text": "Google"}}, {{"text": "Mountain View"}}, {{"text": "AI team"}}]
|
||||
|
||||
### Example 5: When to Split into Multiple Facts
|
||||
**Input:**
|
||||
|
|
@ -270,10 +261,10 @@ Sarah: Sounds amazing! I'll add it to my itinerary."
|
|||
|
||||
**✅ GOOD (split into 2 facts - different topics):**
|
||||
1. "Caroline received a necklace from her grandmother in Sweden"
|
||||
- entities: [{{"text": "Caroline", "type": "PERSON"}}, {{"text": "Sweden", "type": "PLACE"}}]
|
||||
- entities: [{{"text": "Caroline"}}, {{"text": "Sweden"}}]
|
||||
2. "Caroline is planning to visit Stockholm next month to attend a tech conference"
|
||||
- date: Next month from reference
|
||||
- entities: [{{"text": "Caroline", "type": "PERSON"}}, {{"text": "Stockholm", "type": "PLACE"}}]
|
||||
- entities: [{{"text": "Caroline"}}, {{"text": "Stockholm"}}]
|
||||
- NOTE: Split because one is about the past (necklace) and one is future plans (conference) - completely different topics
|
||||
|
||||
## TEXT TO EXTRACT FROM:
|
||||
|
|
@ -303,7 +294,6 @@ Sarah: Sounds amazing! I'll add it to my itinerary."
|
|||
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
llm_call_start = time.time()
|
||||
extraction_response = await llm_config.call(
|
||||
messages=[
|
||||
{
|
||||
|
|
@ -321,11 +311,7 @@ Sarah: Sounds amazing! I'll add it to my itinerary."
|
|||
max_tokens=65000,
|
||||
extra_body={"service_tier": "auto"}
|
||||
)
|
||||
llm_call_time = time.time() - llm_call_start
|
||||
|
||||
# Convert to dict format
|
||||
chunk_facts = [fact.model_dump() for fact in extraction_response.facts]
|
||||
|
||||
return chunk_facts
|
||||
|
||||
except BadRequestError as e:
|
||||
|
|
|
|||
|
|
@ -167,17 +167,17 @@ class LLMConfig:
|
|||
)
|
||||
await asyncio.sleep(sleep_time)
|
||||
else:
|
||||
logger.error(f"Non-retryable API error after {max_retries + 1} attempts: {str(e)}")
|
||||
logger.error(f"Non-retryable API error after {max_retries + 1} attempts: {str(e)}, input {messages}")
|
||||
raise
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error during LLM call: {type(e).__name__}: {str(e)}")
|
||||
logger.error(f"Unexpected error during LLM call: {type(e).__name__}: {str(e)}, input {messages}")
|
||||
raise
|
||||
|
||||
# This should never be reached, but just in case
|
||||
if last_exception:
|
||||
raise last_exception
|
||||
raise RuntimeError("LLM call failed after all retries with no exception captured")
|
||||
raise RuntimeError(f"LLM call failed after all retries with no exception captured, input {messages}")
|
||||
|
||||
@classmethod
|
||||
def for_memory(cls) -> "LLMConfig":
|
||||
|
|
|
|||
|
|
@ -146,7 +146,6 @@ class Entity(Base):
|
|||
UUID(as_uuid=True), primary_key=True, server_default=sql_text("uuid_generate_v4()")
|
||||
)
|
||||
canonical_name: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
entity_type: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
agent_id: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
entity_metadata: Mapped[dict] = mapped_column("metadata", JSONB, server_default=sql_text("'{}'::jsonb"))
|
||||
first_seen: Mapped[datetime] = mapped_column(
|
||||
|
|
@ -176,8 +175,7 @@ class Entity(Base):
|
|||
__table_args__ = (
|
||||
Index("idx_entities_agent_id", "agent_id"),
|
||||
Index("idx_entities_canonical_name", "canonical_name"),
|
||||
Index("idx_entities_type", "entity_type"),
|
||||
Index("idx_entities_agent_name_type", "agent_id", "canonical_name", "entity_type"),
|
||||
Index("idx_entities_agent_name", "agent_id", "canonical_name"),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -46,37 +46,22 @@ class ThinkOperationsMixin:
|
|||
if self._llm_config is None:
|
||||
raise ValueError("Memory LLM API key not set. Set MEMORY_LLM_API_KEY environment variable.")
|
||||
|
||||
# Steps 1-3: Run all three searches in parallel
|
||||
(agent_results, _), (world_results, _), (opinion_results, _) = await asyncio.gather(
|
||||
# Get agent facts (identity)
|
||||
self.search_async(
|
||||
agent_id=agent_id,
|
||||
query=query,
|
||||
thinking_budget=thinking_budget,
|
||||
max_tokens=4096,
|
||||
enable_trace=False,
|
||||
fact_type='agent'
|
||||
),
|
||||
# Get world facts
|
||||
self.search_async(
|
||||
agent_id=agent_id,
|
||||
query=query,
|
||||
thinking_budget=thinking_budget,
|
||||
max_tokens=4096,
|
||||
enable_trace=False,
|
||||
fact_type='world'
|
||||
),
|
||||
# Get existing opinions
|
||||
self.search_async(
|
||||
agent_id=agent_id,
|
||||
query=query,
|
||||
thinking_budget=thinking_budget,
|
||||
max_tokens=4096,
|
||||
enable_trace=False,
|
||||
fact_type='opinion'
|
||||
)
|
||||
# Steps 1-3: Run multi-fact-type search (12-way retrieval: 4 methods × 3 fact types)
|
||||
# This is more efficient than 3 separate searches as it merges and reranks all results together
|
||||
all_results, _ = await self.search_async(
|
||||
agent_id=agent_id,
|
||||
query=query,
|
||||
thinking_budget=thinking_budget,
|
||||
max_tokens=4096,
|
||||
enable_trace=False,
|
||||
fact_type=['agent', 'world', 'opinion']
|
||||
)
|
||||
|
||||
# Split results by fact type for structured response
|
||||
agent_results = [r for r in all_results if r.get('fact_type') == 'agent']
|
||||
world_results = [r for r in all_results if r.get('fact_type') == 'world']
|
||||
opinion_results = [r for r in all_results if r.get('fact_type') == 'opinion']
|
||||
|
||||
# Step 4: Format facts for LLM with full details as JSON
|
||||
import json
|
||||
|
||||
|
|
|
|||
|
|
@ -42,9 +42,7 @@ class RemoteMemoryClient:
|
|||
self,
|
||||
agent_id: str,
|
||||
contents: List[Dict[str, Any]],
|
||||
document_id: Optional[str] = None,
|
||||
document_metadata: Optional[Dict[str, Any]] = None,
|
||||
upsert: bool = False
|
||||
document_id: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Store multiple memory items via API.
|
||||
|
|
@ -52,9 +50,7 @@ class RemoteMemoryClient:
|
|||
Args:
|
||||
agent_id: Agent identifier
|
||||
contents: List of content dicts with 'content', 'event_date', 'context' keys
|
||||
document_id: Optional document identifier
|
||||
document_metadata: Optional document metadata
|
||||
upsert: Whether to upsert (update if exists)
|
||||
document_id: Optional document identifier (always upserts if document exists)
|
||||
|
||||
Returns:
|
||||
Result dict with success status
|
||||
|
|
@ -79,14 +75,11 @@ class RemoteMemoryClient:
|
|||
# Make API request
|
||||
request_data = {
|
||||
"agent_id": agent_id,
|
||||
"items": items,
|
||||
"upsert": upsert
|
||||
"items": items
|
||||
}
|
||||
|
||||
if document_id:
|
||||
request_data["document_id"] = document_id
|
||||
if document_metadata:
|
||||
request_data["document_metadata"] = document_metadata
|
||||
|
||||
response = await self.client.post(
|
||||
f"{self.base_url}/api/memories/batch_async",
|
||||
|
|
@ -103,7 +96,7 @@ class RemoteMemoryClient:
|
|||
max_tokens: int = 4096,
|
||||
enable_trace: bool = False,
|
||||
reranker: str = "heuristic",
|
||||
fact_type: Optional[str] = None
|
||||
fact_type: Optional[List[str]] = None
|
||||
) -> Tuple[List[Dict[str, Any]], Optional[Dict[str, Any]]]:
|
||||
"""
|
||||
Search memories via API.
|
||||
|
|
@ -115,7 +108,7 @@ class RemoteMemoryClient:
|
|||
max_tokens: Maximum tokens to retrieve
|
||||
enable_trace: Whether to return trace information
|
||||
reranker: Reranker type ("heuristic" or other)
|
||||
fact_type: Optional fact type filter (world/agent/opinion)
|
||||
fact_type: Optional list of fact types to search (e.g., ['world', 'agent'])
|
||||
|
||||
Returns:
|
||||
Tuple of (results, trace)
|
||||
|
|
|
|||
|
|
@ -2,19 +2,16 @@
|
|||
Search module for memory retrieval.
|
||||
|
||||
Provides modular search architecture:
|
||||
- Retrieval: 3-way parallel (semantic + BM25 + graph)
|
||||
- Retrieval: 4-way parallel (semantic + BM25 + graph + temporal)
|
||||
- Reranking: Pluggable strategies (heuristic, cross-encoder)
|
||||
- MMR: Diversity enforcement
|
||||
"""
|
||||
|
||||
from .retrieval import retrieve_parallel
|
||||
from .reranking import Reranker, HeuristicReranker, CrossEncoderReranker
|
||||
from .mmr import apply_mmr
|
||||
|
||||
__all__ = [
|
||||
"retrieve_parallel",
|
||||
"Reranker",
|
||||
"HeuristicReranker",
|
||||
"CrossEncoderReranker",
|
||||
"apply_mmr",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,147 +0,0 @@
|
|||
"""
|
||||
Maximal Marginal Relevance (MMR) for diversity in search results.
|
||||
"""
|
||||
|
||||
from typing import List, Dict, Any
|
||||
import numpy as np
|
||||
import json
|
||||
|
||||
|
||||
def apply_mmr(
|
||||
results: List[Dict[str, Any]],
|
||||
top_k: int,
|
||||
mmr_lambda: float,
|
||||
log_buffer: List[str]
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Apply Maximal Marginal Relevance (MMR) to diversify search results.
|
||||
|
||||
MMR balances relevance with diversity by selecting results that are:
|
||||
1. Relevant to the query (high score)
|
||||
2. Different from already selected results (low similarity)
|
||||
|
||||
Formula: MMR = λ * relevance - (1-λ) * max_similarity_to_selected
|
||||
|
||||
Args:
|
||||
results: Sorted list of all results with embeddings
|
||||
top_k: Number of results to select
|
||||
mmr_lambda: Balance parameter (0=max diversity, 1=max relevance)
|
||||
log_buffer: Buffer for logging
|
||||
|
||||
Returns:
|
||||
List of selected results with MMR metadata
|
||||
"""
|
||||
if not results or top_k <= 0:
|
||||
return []
|
||||
|
||||
if len(results) <= top_k:
|
||||
# Not enough results for MMR to matter
|
||||
for idx, result in enumerate(results):
|
||||
result["original_rank"] = idx + 1
|
||||
result["mmr_score"] = None
|
||||
result["mmr_relevance"] = None
|
||||
result["mmr_max_similarity"] = None
|
||||
result["mmr_diversified"] = False
|
||||
result.pop("embedding", None)
|
||||
return results
|
||||
|
||||
# Normalize relevance scores to [0, 1] for fair comparison
|
||||
weights = [r["weight"] for r in results]
|
||||
min_weight = min(weights)
|
||||
max_weight = max(weights)
|
||||
weight_range = max_weight - min_weight
|
||||
|
||||
if weight_range > 0:
|
||||
for r in results:
|
||||
r["_normalized_weight"] = (r["weight"] - min_weight) / weight_range
|
||||
else:
|
||||
for r in results:
|
||||
r["_normalized_weight"] = 1.0
|
||||
|
||||
# Convert embeddings to numpy arrays
|
||||
for r in results:
|
||||
emb = r.get("embedding")
|
||||
if emb is not None:
|
||||
if isinstance(emb, str):
|
||||
emb = json.loads(emb)
|
||||
if not isinstance(emb, np.ndarray):
|
||||
emb = np.array(emb, dtype=np.float64)
|
||||
r["_embedding"] = emb
|
||||
else:
|
||||
r["_embedding"] = None
|
||||
|
||||
# MMR selection
|
||||
selected = []
|
||||
remaining = list(results)
|
||||
diversified_count = 0
|
||||
|
||||
for _ in range(top_k):
|
||||
if not remaining:
|
||||
break
|
||||
|
||||
if not selected:
|
||||
# First result: pick highest relevance
|
||||
best_idx = 0
|
||||
best = remaining[best_idx]
|
||||
best_relevance = best["_normalized_weight"]
|
||||
best_max_similarity = 0.0
|
||||
else:
|
||||
# Subsequent results: balance relevance and diversity
|
||||
best_idx = None
|
||||
best_mmr_score = float('-inf')
|
||||
best_relevance = 0.0
|
||||
best_max_similarity = 0.0
|
||||
|
||||
for idx, candidate in enumerate(remaining):
|
||||
relevance = candidate["_normalized_weight"]
|
||||
|
||||
# Calculate max similarity to already selected results
|
||||
max_similarity = 0.0
|
||||
candidate_emb = candidate.get("_embedding")
|
||||
|
||||
if candidate_emb is not None:
|
||||
for selected_result in selected:
|
||||
selected_emb = selected_result.get("_embedding")
|
||||
if selected_emb is not None:
|
||||
# Cosine similarity
|
||||
dot_product = np.dot(candidate_emb, selected_emb)
|
||||
norm_candidate = np.linalg.norm(candidate_emb)
|
||||
norm_selected = np.linalg.norm(selected_emb)
|
||||
if norm_candidate > 0 and norm_selected > 0:
|
||||
similarity = dot_product / (norm_candidate * norm_selected)
|
||||
max_similarity = max(max_similarity, similarity)
|
||||
|
||||
# MMR score
|
||||
mmr_score = mmr_lambda * relevance - (1 - mmr_lambda) * max_similarity
|
||||
|
||||
if mmr_score > best_mmr_score:
|
||||
best_mmr_score = mmr_score
|
||||
best_idx = idx
|
||||
best_relevance = relevance
|
||||
best_max_similarity = max_similarity
|
||||
|
||||
# Select best result
|
||||
best = remaining.pop(best_idx)
|
||||
best["original_rank"] = len(selected) + 1
|
||||
best["mmr_score"] = best_mmr_score if selected else best_relevance
|
||||
best["mmr_relevance"] = best_relevance
|
||||
best["mmr_max_similarity"] = best_max_similarity
|
||||
|
||||
# Check if this was a diversified pick (not top of remaining by relevance)
|
||||
if selected and best_idx > 0:
|
||||
best["mmr_diversified"] = True
|
||||
diversified_count += 1
|
||||
else:
|
||||
best["mmr_diversified"] = False
|
||||
|
||||
selected.append(best)
|
||||
|
||||
# Clean up temporary fields and embeddings
|
||||
for r in selected:
|
||||
r.pop("_normalized_weight", None)
|
||||
r.pop("_embedding", None)
|
||||
r.pop("embedding", None)
|
||||
|
||||
log_buffer.append(f" MMR: Selected {len(selected)} results, {diversified_count} diversified picks")
|
||||
|
||||
return selected
|
||||
|
|
@ -35,7 +35,7 @@ async def retrieve_semantic(
|
|||
"""
|
||||
results = await conn.fetch(
|
||||
"""
|
||||
SELECT id, text, context, event_date, access_count, embedding,
|
||||
SELECT id, text, context, event_date, access_count, embedding, fact_type,
|
||||
1 - (embedding <=> $1::vector) AS similarity
|
||||
FROM memory_units
|
||||
WHERE agent_id = $2
|
||||
|
|
@ -70,13 +70,26 @@ async def retrieve_bm25(
|
|||
Returns:
|
||||
List of (doc_id, data) tuples
|
||||
"""
|
||||
import re
|
||||
|
||||
# Sanitize query text: remove special characters that have meaning in tsquery
|
||||
# Keep only alphanumeric characters and spaces
|
||||
sanitized_text = re.sub(r'[^\w\s]', ' ', query_text.lower())
|
||||
|
||||
# Split and filter empty strings
|
||||
tokens = [token for token in sanitized_text.split() if token]
|
||||
|
||||
if not tokens:
|
||||
# If no valid tokens, return empty results
|
||||
return []
|
||||
|
||||
# Convert query to tsquery using OR for more flexible matching
|
||||
# This prevents empty results when some terms are missing
|
||||
query_tsquery = " | ".join(query_text.lower().split())
|
||||
query_tsquery = " | ".join(tokens)
|
||||
|
||||
results = await conn.fetch(
|
||||
"""
|
||||
SELECT id, text, context, event_date, access_count, embedding,
|
||||
SELECT id, text, context, event_date, access_count, embedding, fact_type,
|
||||
ts_rank_cd(search_vector, to_tsquery('english', $1)) AS bm25_score
|
||||
FROM memory_units
|
||||
WHERE agent_id = $2
|
||||
|
|
@ -113,7 +126,7 @@ async def retrieve_graph(
|
|||
# Find entry points
|
||||
entry_points = await conn.fetch(
|
||||
"""
|
||||
SELECT id, text, context, event_date, access_count, embedding,
|
||||
SELECT id, text, context, event_date, access_count, embedding, fact_type,
|
||||
1 - (embedding <=> $1::vector) AS similarity
|
||||
FROM memory_units
|
||||
WHERE agent_id = $2
|
||||
|
|
@ -150,7 +163,7 @@ async def retrieve_graph(
|
|||
if budget_remaining > 0:
|
||||
neighbors = await conn.fetch(
|
||||
"""
|
||||
SELECT mu.id, mu.text, mu.context, mu.event_date, mu.access_count, mu.embedding,
|
||||
SELECT mu.id, mu.text, mu.context, mu.event_date, mu.access_count, mu.embedding, mu.fact_type,
|
||||
ml.weight
|
||||
FROM memory_links ml
|
||||
JOIN memory_units mu ON ml.to_unit_id = mu.id
|
||||
|
|
@ -204,10 +217,18 @@ async def retrieve_temporal(
|
|||
Returns:
|
||||
List of (doc_id, data) tuples with temporal_score
|
||||
"""
|
||||
from datetime import timezone
|
||||
|
||||
# Ensure start_date and end_date are timezone-aware (UTC) to match database datetimes
|
||||
if start_date.tzinfo is None:
|
||||
start_date = start_date.replace(tzinfo=timezone.utc)
|
||||
if end_date.tzinfo is None:
|
||||
end_date = end_date.replace(tzinfo=timezone.utc)
|
||||
|
||||
# Find entry points: facts in date range with semantic relevance
|
||||
entry_points = await conn.fetch(
|
||||
"""
|
||||
SELECT id, text, context, event_date, access_count, embedding,
|
||||
SELECT id, text, context, event_date, access_count, embedding, fact_type,
|
||||
1 - (embedding <=> $1::vector) AS similarity
|
||||
FROM memory_units
|
||||
WHERE agent_id = $2
|
||||
|
|
@ -226,6 +247,7 @@ async def retrieve_temporal(
|
|||
|
||||
# Calculate temporal scores for entry points
|
||||
total_days = (end_date - start_date).total_seconds() / 86400
|
||||
mid_date = start_date + (end_date - start_date) / 2 # Calculate once for all comparisons
|
||||
results = []
|
||||
visited = set()
|
||||
|
||||
|
|
@ -235,7 +257,6 @@ async def retrieve_temporal(
|
|||
|
||||
# Temporal proximity score (closer to range center = higher score)
|
||||
event_date = ep["event_date"]
|
||||
mid_date = start_date + (end_date - start_date) / 2
|
||||
days_from_mid = abs((event_date - mid_date).total_seconds() / 86400)
|
||||
temporal_proximity = 1.0 - min(days_from_mid / (total_days / 2), 1.0) if total_days > 0 else 1.0
|
||||
|
||||
|
|
@ -256,7 +277,7 @@ async def retrieve_temporal(
|
|||
if budget_remaining > 0:
|
||||
neighbors = await conn.fetch(
|
||||
"""
|
||||
SELECT mu.id, mu.text, mu.context, mu.event_date, mu.access_count, mu.embedding,
|
||||
SELECT mu.id, mu.text, mu.context, mu.event_date, mu.access_count, mu.embedding, mu.fact_type,
|
||||
ml.weight, ml.link_type,
|
||||
1 - (mu.embedding <=> $1::vector) AS similarity
|
||||
FROM memory_links ml
|
||||
|
|
@ -313,7 +334,8 @@ async def retrieve_parallel(
|
|||
query_embedding_str: str,
|
||||
agent_id: str,
|
||||
fact_type: str,
|
||||
thinking_budget: int
|
||||
thinking_budget: int,
|
||||
question_date: Optional[datetime] = None
|
||||
) -> Tuple[List, List, List, Optional[List]]:
|
||||
"""
|
||||
Run 3-way or 4-way parallel retrieval (adds temporal if detected).
|
||||
|
|
@ -325,6 +347,7 @@ async def retrieve_parallel(
|
|||
agent_id: Agent ID
|
||||
fact_type: Fact type to filter
|
||||
thinking_budget: Budget for graph traversal and retrieval limits
|
||||
question_date: Optional date when question was asked (for temporal filtering)
|
||||
|
||||
Returns:
|
||||
Tuple of (semantic_results, bm25_results, graph_results, temporal_results)
|
||||
|
|
@ -332,7 +355,7 @@ async def retrieve_parallel(
|
|||
"""
|
||||
# Detect temporal constraint
|
||||
from .temporal_extraction import extract_temporal_constraint
|
||||
temporal_constraint = extract_temporal_constraint(query_text)
|
||||
temporal_constraint = extract_temporal_constraint(query_text, reference_date=question_date)
|
||||
|
||||
# Each retrieval needs its own connection
|
||||
async def run_semantic():
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ This implements a sophisticated memory architecture that combines:
|
|||
"""
|
||||
import os
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
import asyncpg
|
||||
import asyncio
|
||||
from .embeddings import Embeddings, SentenceTransformersEmbeddings
|
||||
|
|
@ -187,23 +187,19 @@ class TemporalSemanticMemory(
|
|||
Handler for batch put tasks.
|
||||
|
||||
Args:
|
||||
task_dict: Dict with 'agent_id', 'contents', 'document_id', 'document_metadata', 'upsert'
|
||||
task_dict: Dict with 'agent_id', 'contents', 'document_id'
|
||||
"""
|
||||
try:
|
||||
agent_id = task_dict.get('agent_id')
|
||||
contents = task_dict.get('contents', [])
|
||||
document_id = task_dict.get('document_id')
|
||||
document_metadata = task_dict.get('document_metadata')
|
||||
upsert = task_dict.get('upsert', False)
|
||||
|
||||
logger.info(f"[BATCH_PUT_TASK] Starting background batch put for agent_id={agent_id}, {len(contents)} items")
|
||||
|
||||
await self.put_batch_async(
|
||||
agent_id=agent_id,
|
||||
contents=contents,
|
||||
document_id=document_id,
|
||||
document_metadata=document_metadata,
|
||||
upsert=upsert
|
||||
document_id=document_id
|
||||
)
|
||||
|
||||
logger.info(f"[BATCH_PUT_TASK] Completed background batch put for agent_id={agent_id}")
|
||||
|
|
@ -536,8 +532,6 @@ class TemporalSemanticMemory(
|
|||
context: str = "",
|
||||
event_date: Optional[datetime] = None,
|
||||
document_id: Optional[str] = None,
|
||||
document_metadata: Optional[Dict[str, Any]] = None,
|
||||
upsert: bool = False,
|
||||
fact_type_override: Optional[str] = None,
|
||||
confidence_score: Optional[float] = None,
|
||||
) -> List[str]:
|
||||
|
|
@ -551,9 +545,7 @@ class TemporalSemanticMemory(
|
|||
content: Text content to store
|
||||
context: Context about when/why this memory was formed
|
||||
event_date: When the event occurred (defaults to now)
|
||||
document_id: Optional document ID for tracking and upsert
|
||||
document_metadata: Optional metadata about the document
|
||||
upsert: If True and document_id exists, delete old units and create new ones
|
||||
document_id: Optional document ID for tracking (always upserts if document already exists)
|
||||
fact_type_override: Override fact type ('world', 'agent', 'opinion')
|
||||
confidence_score: Confidence score for opinions (0.0 to 1.0)
|
||||
|
||||
|
|
@ -569,8 +561,6 @@ class TemporalSemanticMemory(
|
|||
"event_date": event_date
|
||||
}],
|
||||
document_id=document_id,
|
||||
document_metadata=document_metadata,
|
||||
upsert=upsert,
|
||||
fact_type_override=fact_type_override,
|
||||
confidence_score=confidence_score
|
||||
)
|
||||
|
|
@ -583,8 +573,6 @@ class TemporalSemanticMemory(
|
|||
agent_id: str,
|
||||
contents: List[Dict[str, Any]],
|
||||
document_id: Optional[str] = None,
|
||||
document_metadata: Optional[Dict[str, Any]] = None,
|
||||
upsert: bool = False,
|
||||
fact_type_override: Optional[str] = None,
|
||||
confidence_score: Optional[float] = None,
|
||||
) -> List[List[str]]:
|
||||
|
|
@ -603,9 +591,7 @@ class TemporalSemanticMemory(
|
|||
- "content" (required): Text content to store
|
||||
- "context" (optional): Context about the memory
|
||||
- "event_date" (optional): When the event occurred
|
||||
document_id: Optional document ID for tracking and upsert
|
||||
document_metadata: Optional metadata about the document
|
||||
upsert: If True and document_id exists, delete old units and create new ones
|
||||
document_id: Optional document ID for tracking (always upserts if document already exists)
|
||||
fact_type_override: Override fact type for all facts ('world', 'agent', 'opinion')
|
||||
confidence_score: Confidence score for opinions (0.0 to 1.0)
|
||||
|
||||
|
|
@ -619,8 +605,7 @@ class TemporalSemanticMemory(
|
|||
{"content": "Alice works at Google", "context": "conversation"},
|
||||
{"content": "Bob loves Python", "context": "conversation"},
|
||||
],
|
||||
document_id="meeting-2024-01-15",
|
||||
upsert=True
|
||||
document_id="meeting-2024-01-15"
|
||||
)
|
||||
# Returns: [["unit-id-1"], ["unit-id-2"]]
|
||||
"""
|
||||
|
|
@ -672,8 +657,7 @@ class TemporalSemanticMemory(
|
|||
agent_id=agent_id,
|
||||
contents=sub_batch,
|
||||
document_id=document_id,
|
||||
document_metadata=document_metadata,
|
||||
upsert=upsert and i == 1, # Only upsert on first batch
|
||||
is_first_batch=i == 1, # Only upsert on first batch
|
||||
fact_type_override=fact_type_override,
|
||||
confidence_score=confidence_score
|
||||
)
|
||||
|
|
@ -688,8 +672,7 @@ class TemporalSemanticMemory(
|
|||
agent_id=agent_id,
|
||||
contents=contents,
|
||||
document_id=document_id,
|
||||
document_metadata=document_metadata,
|
||||
upsert=upsert,
|
||||
is_first_batch=True,
|
||||
fact_type_override=fact_type_override,
|
||||
confidence_score=confidence_score
|
||||
)
|
||||
|
|
@ -699,8 +682,7 @@ class TemporalSemanticMemory(
|
|||
agent_id: str,
|
||||
contents: List[Dict[str, Any]],
|
||||
document_id: Optional[str] = None,
|
||||
document_metadata: Optional[Dict[str, Any]] = None,
|
||||
upsert: bool = False,
|
||||
is_first_batch: bool = True,
|
||||
fact_type_override: Optional[str] = None,
|
||||
confidence_score: Optional[float] = None,
|
||||
) -> List[List[str]]:
|
||||
|
|
@ -711,6 +693,14 @@ class TemporalSemanticMemory(
|
|||
Called by put_batch_async after chunking large batches.
|
||||
|
||||
Uses semaphore for backpressure to limit concurrent puts.
|
||||
|
||||
Args:
|
||||
agent_id: Unique identifier for the agent
|
||||
contents: List of dicts with content, context, event_date
|
||||
document_id: Optional document ID (always upserts if exists)
|
||||
is_first_batch: Whether this is the first batch (for chunked operations, only delete on first batch)
|
||||
fact_type_override: Override fact type for all facts
|
||||
confidence_score: Confidence score for opinions
|
||||
"""
|
||||
# Backpressure: limit concurrent puts to prevent database contention
|
||||
async with self._put_semaphore:
|
||||
|
|
@ -803,7 +793,7 @@ class TemporalSemanticMemory(
|
|||
async with conn.transaction():
|
||||
logger.debug("Inside transaction")
|
||||
try:
|
||||
# Handle document tracking and upsert
|
||||
# Handle document tracking with automatic upsert
|
||||
if document_id:
|
||||
logger.debug(f"Handling document tracking for {document_id}")
|
||||
import hashlib
|
||||
|
|
@ -813,8 +803,9 @@ class TemporalSemanticMemory(
|
|||
combined_content = "\n".join([c.get("content", "") for c in contents])
|
||||
content_hash = hashlib.sha256(combined_content.encode()).hexdigest()
|
||||
|
||||
# If upsert, delete old document first (cascades to units and links)
|
||||
if upsert:
|
||||
# Always delete old document first if it exists (cascades to units and links)
|
||||
# Only delete on the first batch to avoid deleting data we just inserted
|
||||
if is_first_batch:
|
||||
deleted = await conn.fetchval(
|
||||
"DELETE FROM documents WHERE id = $1 AND agent_id = $2 RETURNING id",
|
||||
document_id, agent_id
|
||||
|
|
@ -822,8 +813,8 @@ class TemporalSemanticMemory(
|
|||
if deleted:
|
||||
logger.debug(f"[3.1] Upsert: Deleted existing document '{document_id}' and all its units")
|
||||
|
||||
# Insert or update document
|
||||
# Always use ON CONFLICT for idempotent behavior
|
||||
# Insert document (or update if exists from concurrent operations)
|
||||
# Use ON CONFLICT for idempotent behavior in edge cases
|
||||
await conn.execute(
|
||||
"""
|
||||
INSERT INTO documents (id, agent_id, original_text, content_hash, metadata)
|
||||
|
|
@ -838,7 +829,7 @@ class TemporalSemanticMemory(
|
|||
agent_id,
|
||||
combined_content,
|
||||
content_hash,
|
||||
json.dumps(document_metadata or {})
|
||||
json.dumps({}) # Empty metadata dict
|
||||
)
|
||||
logger.debug(f"[3.2] Document '{document_id}' stored/updated")
|
||||
|
||||
|
|
@ -1031,17 +1022,18 @@ class TemporalSemanticMemory(
|
|||
self,
|
||||
agent_id: str,
|
||||
query: str,
|
||||
fact_type: str,
|
||||
fact_type: List[str],
|
||||
thinking_budget: int = 50,
|
||||
max_tokens: int = 4096,
|
||||
enable_trace: bool = False,
|
||||
reranker: str = "cross-encoder",
|
||||
question_date: Optional[datetime] = None,
|
||||
) -> tuple[List[Dict[str, Any]], Optional[Any]]:
|
||||
"""
|
||||
Search memories using 4-way parallel retrieval (semantic + keyword + graph + temporal).
|
||||
Search memories using N*4-way parallel retrieval (N fact types × 4 retrieval methods).
|
||||
|
||||
This implements the core SEARCH operation:
|
||||
1. Retrieval: Run 4 parallel retrievals (semantic vector, BM25 keyword, graph activation, temporal graph)
|
||||
1. Retrieval: For each fact type, run 4 parallel retrievals (semantic vector, BM25 keyword, graph activation, temporal graph)
|
||||
2. Merge: Combine using Reciprocal Rank Fusion (RRF)
|
||||
3. Rerank: Score using selected reranker (heuristic or cross-encoder)
|
||||
4. Diversify: Apply MMR for diversity
|
||||
|
|
@ -1050,7 +1042,7 @@ class TemporalSemanticMemory(
|
|||
Args:
|
||||
agent_id: Agent ID to search for
|
||||
query: Search query
|
||||
fact_type: Type of facts to search ('world', 'agent', 'opinion')
|
||||
fact_type: List of fact types to search (e.g., ['world', 'agent'])
|
||||
thinking_budget: How many units to explore in graph traversal (controls compute cost)
|
||||
max_tokens: Maximum tokens to return (counts only 'text' field, default 4096)
|
||||
Results are returned until token budget is reached, stopping before
|
||||
|
|
@ -1059,6 +1051,7 @@ class TemporalSemanticMemory(
|
|||
reranker: Reranking strategy - "heuristic" (default) or "cross-encoder"
|
||||
- heuristic: 60% semantic + 40% BM25 + normalized boosts (fast)
|
||||
- cross-encoder: Neural reranking with ms-marco-MiniLM-L-6-v2 (slower but more accurate)
|
||||
question_date: Optional date when question was asked (for temporal filtering)
|
||||
|
||||
Returns:
|
||||
Tuple of (results, trace) where results is a list of memory units
|
||||
|
|
@ -1071,7 +1064,7 @@ class TemporalSemanticMemory(
|
|||
for attempt in range(max_retries + 1):
|
||||
try:
|
||||
return await self._search_with_retries(
|
||||
agent_id, query, fact_type, thinking_budget, max_tokens, enable_trace, reranker
|
||||
agent_id, query, fact_type, thinking_budget, max_tokens, enable_trace, reranker, question_date
|
||||
)
|
||||
except Exception as e:
|
||||
# Check if it's a connection error
|
||||
|
|
@ -1097,11 +1090,12 @@ class TemporalSemanticMemory(
|
|||
self,
|
||||
agent_id: str,
|
||||
query: str,
|
||||
fact_type: str,
|
||||
fact_type: List[str],
|
||||
thinking_budget: int,
|
||||
max_tokens: int,
|
||||
enable_trace: bool,
|
||||
reranker: str,
|
||||
question_date: Optional[datetime] = None,
|
||||
) -> tuple[List[Dict[str, Any]], Optional[Any]]:
|
||||
"""
|
||||
Search implementation with modular retrieval and reranking.
|
||||
|
|
@ -1150,7 +1144,7 @@ class TemporalSemanticMemory(
|
|||
tracer.record_query_embedding(query_embedding)
|
||||
tracer.add_phase_metric("generate_query_embedding", step_duration)
|
||||
|
||||
# Step 2: 3-Way or 4-Way Parallel Retrieval
|
||||
# Step 2: N*4-Way Parallel Retrieval (N fact types × 4 retrieval methods)
|
||||
step_start = time.time()
|
||||
query_embedding_str = str(query_embedding)
|
||||
|
||||
|
|
@ -1158,16 +1152,39 @@ class TemporalSemanticMemory(
|
|||
|
||||
# Track each retrieval start time
|
||||
retrieval_start = time.time()
|
||||
semantic_results, bm25_results, graph_results, temporal_results = await retrieve_parallel(
|
||||
pool, query, query_embedding_str, agent_id, fact_type, thinking_budget
|
||||
)
|
||||
|
||||
# Run retrieval for each fact type in parallel
|
||||
retrieval_tasks = [
|
||||
retrieve_parallel(pool, query, query_embedding_str, agent_id, ft, thinking_budget, question_date)
|
||||
for ft in fact_type
|
||||
]
|
||||
all_retrievals = await asyncio.gather(*retrieval_tasks)
|
||||
|
||||
# Combine all results from all fact types
|
||||
semantic_results = []
|
||||
bm25_results = []
|
||||
graph_results = []
|
||||
temporal_results = []
|
||||
|
||||
for ft_semantic, ft_bm25, ft_graph, ft_temporal in all_retrievals:
|
||||
semantic_results.extend(ft_semantic)
|
||||
bm25_results.extend(ft_bm25)
|
||||
graph_results.extend(ft_graph)
|
||||
if ft_temporal:
|
||||
temporal_results.extend(ft_temporal)
|
||||
|
||||
# If no temporal results from any fact type, set to None
|
||||
if not temporal_results:
|
||||
temporal_results = None
|
||||
|
||||
retrieval_duration = time.time() - retrieval_start
|
||||
|
||||
step_duration = time.time() - step_start
|
||||
total_retrievals = len(fact_type) * (4 if temporal_results else 3)
|
||||
if temporal_results:
|
||||
log_buffer.append(f" [2] 4-way retrieval: semantic={len(semantic_results)}, bm25={len(bm25_results)}, graph={len(graph_results)}, temporal={len(temporal_results)} in {step_duration:.3f}s")
|
||||
log_buffer.append(f" [2] {total_retrievals}-way retrieval ({len(fact_type)} fact_types): semantic={len(semantic_results)}, bm25={len(bm25_results)}, graph={len(graph_results)}, temporal={len(temporal_results)} in {step_duration:.3f}s")
|
||||
else:
|
||||
log_buffer.append(f" [2] 3-way retrieval: semantic={len(semantic_results)}, bm25={len(bm25_results)}, graph={len(graph_results)} in {step_duration:.3f}s")
|
||||
log_buffer.append(f" [2] {total_retrievals}-way retrieval ({len(fact_type)} fact_types): semantic={len(semantic_results)}, bm25={len(bm25_results)}, graph={len(graph_results)} in {step_duration:.3f}s")
|
||||
|
||||
# Record retrieval results for tracer
|
||||
if tracer:
|
||||
|
|
@ -1303,25 +1320,12 @@ class TemporalSemanticMemory(
|
|||
"candidates_reranked": len(results)
|
||||
})
|
||||
|
||||
# Step 6: Apply MMR (always enabled with λ=0.5)
|
||||
step_start = time.time()
|
||||
from .search.mmr import apply_mmr
|
||||
# Step 5: Truncate to thinking_budget * 2 for token filtering
|
||||
rerank_limit = thinking_budget * 2
|
||||
top_results = results[:rerank_limit]
|
||||
log_buffer.append(f" [5] Truncated to top {len(top_results)} results")
|
||||
|
||||
mmr_lambda = 0.5
|
||||
# MMR also uses thinking_budget * 2 to have diverse options for token filtering
|
||||
mmr_limit = thinking_budget * 2
|
||||
top_results = apply_mmr(results, mmr_limit, mmr_lambda, log_buffer)
|
||||
|
||||
step_duration = time.time() - step_start
|
||||
log_buffer.append(f" [5] MMR diversification (λ={mmr_lambda}): {step_duration:.3f}s")
|
||||
|
||||
if tracer:
|
||||
tracer.add_phase_metric("mmr_diversification", step_duration, {
|
||||
"lambda": mmr_lambda,
|
||||
"results_selected": len(top_results)
|
||||
})
|
||||
|
||||
# Step 7: Token budget filtering
|
||||
# Step 6: Token budget filtering
|
||||
step_start = time.time()
|
||||
|
||||
# Filter results to fit within max_tokens budget
|
||||
|
|
@ -1428,132 +1432,6 @@ class TemporalSemanticMemory(
|
|||
|
||||
return filtered_results, total_tokens
|
||||
|
||||
def _apply_mmr(
|
||||
self,
|
||||
results: List[Dict[str, Any]],
|
||||
top_k: int,
|
||||
mmr_lambda: float,
|
||||
log_buffer: List[str]
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Apply Maximal Marginal Relevance (MMR) to diversify search results.
|
||||
|
||||
MMR balances relevance with diversity by selecting results that are:
|
||||
1. Relevant to the query (high score)
|
||||
2. Different from already selected results (low similarity)
|
||||
|
||||
Formula: MMR = λ * relevance - (1-λ) * max_similarity_to_selected
|
||||
|
||||
Args:
|
||||
results: Sorted list of all results with embeddings
|
||||
top_k: Number of results to select
|
||||
mmr_lambda: Balance parameter (0=max diversity, 1=max relevance)
|
||||
log_buffer: Logging buffer
|
||||
|
||||
Returns:
|
||||
Diversified list of top_k results
|
||||
"""
|
||||
if not results or top_k <= 0:
|
||||
return []
|
||||
|
||||
# Normalize weights to [0, 1] for fair comparison with similarity
|
||||
max_weight = max(r["weight"] for r in results)
|
||||
min_weight = min(r["weight"] for r in results)
|
||||
weight_range = max_weight - min_weight if max_weight > min_weight else 1.0
|
||||
|
||||
# Pre-compute normalized relevance scores for all results
|
||||
for idx, result in enumerate(results):
|
||||
result["original_rank"] = idx + 1
|
||||
result["normalized_relevance"] = (result["weight"] - min_weight) / weight_range
|
||||
|
||||
# Extract embeddings as a numpy array for vectorized operations
|
||||
# Shape: (num_results, embedding_dim)
|
||||
embeddings_list = []
|
||||
valid_indices = []
|
||||
for idx, result in enumerate(results):
|
||||
if result.get("embedding") is not None:
|
||||
embeddings_list.append(result["embedding"])
|
||||
valid_indices.append(idx)
|
||||
|
||||
if not embeddings_list:
|
||||
# No embeddings available, just return top-k by relevance
|
||||
return results[:top_k]
|
||||
|
||||
# Stack embeddings into a matrix (num_results, embedding_dim)
|
||||
embeddings_matrix = np.array(embeddings_list, dtype=np.float32)
|
||||
|
||||
# Normalize embeddings for faster cosine similarity (just dot product after normalization)
|
||||
norms = np.linalg.norm(embeddings_matrix, axis=1, keepdims=True)
|
||||
norms[norms == 0] = 1.0 # Avoid division by zero
|
||||
embeddings_matrix = embeddings_matrix / norms
|
||||
|
||||
selected_indices = []
|
||||
remaining_indices = list(range(len(results)))
|
||||
diversified_count = 0
|
||||
|
||||
for selection_round in range(min(top_k, len(results))):
|
||||
if not remaining_indices:
|
||||
break
|
||||
|
||||
best_mmr_score = float('-inf')
|
||||
best_remaining_idx = 0
|
||||
|
||||
# Vectorized computation for all remaining candidates
|
||||
for remaining_idx, candidate_idx in enumerate(remaining_indices):
|
||||
candidate = results[candidate_idx]
|
||||
normalized_relevance = candidate["normalized_relevance"]
|
||||
|
||||
# Calculate max similarity to selected results
|
||||
max_similarity = 0.0
|
||||
if selected_indices and candidate_idx in valid_indices:
|
||||
# Find position in embeddings_matrix
|
||||
embedding_idx = valid_indices.index(candidate_idx)
|
||||
candidate_embedding = embeddings_matrix[embedding_idx]
|
||||
|
||||
# Vectorized similarity calculation with all selected embeddings
|
||||
if selected_indices:
|
||||
selected_embedding_indices = [valid_indices.index(idx) for idx in selected_indices if idx in valid_indices]
|
||||
if selected_embedding_indices:
|
||||
selected_embeddings = embeddings_matrix[selected_embedding_indices]
|
||||
# Compute cosine similarities in one operation (already normalized, so just dot product)
|
||||
similarities = np.dot(selected_embeddings, candidate_embedding)
|
||||
max_similarity = float(np.max(similarities))
|
||||
|
||||
# MMR score: balance relevance and diversity
|
||||
mmr_score = mmr_lambda * normalized_relevance - (1 - mmr_lambda) * max_similarity
|
||||
|
||||
if mmr_score > best_mmr_score:
|
||||
best_mmr_score = mmr_score
|
||||
best_remaining_idx = remaining_idx
|
||||
best_max_similarity = max_similarity
|
||||
|
||||
# Select the best candidate
|
||||
best_candidate_idx = remaining_indices.pop(best_remaining_idx)
|
||||
best_candidate = results[best_candidate_idx]
|
||||
|
||||
# Store MMR metadata
|
||||
best_candidate["mmr_score"] = best_mmr_score
|
||||
best_candidate["mmr_relevance"] = best_candidate["normalized_relevance"]
|
||||
best_candidate["mmr_max_similarity"] = best_max_similarity
|
||||
best_candidate["mmr_diversified"] = best_remaining_idx > 0
|
||||
|
||||
selected_indices.append(best_candidate_idx)
|
||||
|
||||
if best_remaining_idx > 0:
|
||||
diversified_count += 1
|
||||
|
||||
log_buffer.append(f" MMR: Selected {len(selected_indices)} results, {diversified_count} diversified picks")
|
||||
|
||||
# Return selected results in order
|
||||
selected_results = [results[idx] for idx in selected_indices]
|
||||
|
||||
# Remove embeddings from final results (not needed in response)
|
||||
for result in selected_results:
|
||||
result.pop("embedding", None)
|
||||
result.pop("normalized_relevance", None) # Clean up temp field
|
||||
|
||||
return selected_results
|
||||
|
||||
async def get_document(self, document_id: str, agent_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Retrieve document metadata and statistics.
|
||||
|
|
@ -1772,7 +1650,7 @@ class TemporalSemanticMemory(
|
|||
|
||||
# Get entity information
|
||||
unit_entities = await conn.fetch("""
|
||||
SELECT ue.unit_id, e.canonical_name, e.entity_type
|
||||
SELECT ue.unit_id, e.canonical_name
|
||||
FROM unit_entities ue
|
||||
JOIN entities e ON ue.entity_id = e.id
|
||||
ORDER BY ue.unit_id
|
||||
|
|
@ -1783,10 +1661,9 @@ class TemporalSemanticMemory(
|
|||
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})")
|
||||
entity_map[unit_id].append(entity_name)
|
||||
|
||||
# Build nodes
|
||||
nodes = []
|
||||
|
|
@ -1952,7 +1829,7 @@ class TemporalSemanticMemory(
|
|||
if units:
|
||||
unit_ids = [row['id'] for row in units]
|
||||
unit_entities = await conn.fetch("""
|
||||
SELECT ue.unit_id, e.canonical_name, e.entity_type
|
||||
SELECT ue.unit_id, e.canonical_name
|
||||
FROM unit_entities ue
|
||||
JOIN entities e ON ue.entity_id = e.id
|
||||
WHERE ue.unit_id = ANY($1::uuid[])
|
||||
|
|
@ -1966,10 +1843,9 @@ class TemporalSemanticMemory(
|
|||
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})")
|
||||
entity_map[unit_id].append(entity_name)
|
||||
|
||||
# Build result items
|
||||
items = []
|
||||
|
|
|
|||
|
|
@ -1,880 +0,0 @@
|
|||
body {
|
||||
font-family: Tahoma, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
/* Breadcrumb */
|
||||
.breadcrumb-container {
|
||||
background: #333;
|
||||
color: white;
|
||||
padding: 12px 20px;
|
||||
border-bottom: 3px solid #42a5f5;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.breadcrumb-separator {
|
||||
color: #999;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.agent-selector {
|
||||
padding: 6px 12px;
|
||||
border: 2px solid #42a5f5;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.agent-selector:hover {
|
||||
background: #e3f2fd;
|
||||
border-color: #1e88e5;
|
||||
}
|
||||
|
||||
.agent-selector:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba(66, 165, 245, 0.3);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
/* Data Tab Styles */
|
||||
.data-sub-tabs {
|
||||
background: #e3f2fd;
|
||||
padding: 10px 20px;
|
||||
border-bottom: 2px solid #333;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.data-sub-tab-button {
|
||||
background: #fff;
|
||||
border: 2px solid #42a5f5;
|
||||
padding: 8px 20px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.data-sub-tab-button:hover {
|
||||
background: #e3f2fd;
|
||||
}
|
||||
|
||||
.data-sub-tab-button.active {
|
||||
background: #42a5f5;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.data-subtab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.data-subtab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.view-toggle {
|
||||
background: #f9f9f9;
|
||||
padding: 10px 20px;
|
||||
border-bottom: 2px solid #ddd;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.view-toggle-button {
|
||||
background: #fff;
|
||||
border: 2px solid #ccc;
|
||||
padding: 6px 16px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.view-toggle-button:hover {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.view-toggle-button.active {
|
||||
background: #333;
|
||||
color: white;
|
||||
border-color: #333;
|
||||
}
|
||||
|
||||
.no-agent-message {
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.data-view {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.data-controls {
|
||||
padding: 15px;
|
||||
background: #f9f9f9;
|
||||
border-bottom: 2px solid #333;
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.data-controls h2 {
|
||||
margin: 0;
|
||||
flex: 1 0 100%;
|
||||
}
|
||||
|
||||
.load-button {
|
||||
padding: 8px 20px;
|
||||
background: #66bb6a;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.load-button:hover {
|
||||
background: #43a047;
|
||||
}
|
||||
|
||||
.control-input {
|
||||
width: 80px;
|
||||
padding: 5px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.control-select {
|
||||
padding: 5px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.apply-button {
|
||||
padding: 6px 15px;
|
||||
background: #42a5f5;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.apply-button:hover {
|
||||
background: #1e88e5;
|
||||
}
|
||||
|
||||
.node-count {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.graph-canvas {
|
||||
width: 100%;
|
||||
height: 800px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.table-filter {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
padding: 10px;
|
||||
margin: 0 20px 15px 20px;
|
||||
border: 2px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
padding: 0 20px 20px 20px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.empty-message {
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
#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 h4 {
|
||||
margin: 10px 0 5px 0;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
margin: 8px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.legend-line {
|
||||
width: 30px;
|
||||
height: 2px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.legend-line.temporal {
|
||||
background: #00bcd4;
|
||||
border-top: 1px dashed #00bcd4;
|
||||
}
|
||||
|
||||
.legend-line.semantic {
|
||||
background: #ff69b4;
|
||||
}
|
||||
|
||||
.legend-line.entity {
|
||||
background: #ffd700;
|
||||
}
|
||||
|
||||
.legend-node {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 10px;
|
||||
border: 1px solid #999;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.legend-node.no-entities {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.legend-node.one-entity {
|
||||
background: #90caf9;
|
||||
}
|
||||
|
||||
.legend-node.multi-entities {
|
||||
background: #42a5f5;
|
||||
}
|
||||
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.retrieval-tabs {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
padding: 10px 15px;
|
||||
background: #f5f5f5;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.retrieval-tab-btn {
|
||||
padding: 8px 16px;
|
||||
background: #e0e0e0;
|
||||
color: #333;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px 4px 0 0;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.retrieval-tab-btn:hover {
|
||||
background: #d0d0d0;
|
||||
}
|
||||
|
||||
.retrieval-tab-btn.active {
|
||||
background: white;
|
||||
color: #1e88e5;
|
||||
border-bottom: 2px solid white;
|
||||
border-color: #1e88e5 #1e88e5 white #1e88e5;
|
||||
}
|
||||
|
||||
.retrieval-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.debug-section {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.debug-viz-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
/* Think Tab Styles */
|
||||
#think-tab {
|
||||
padding: 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.think-controls {
|
||||
background: #f9f9f9;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid #333;
|
||||
}
|
||||
|
||||
.think-answer {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid #333;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.think-sources {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid #333;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
/* Delete Button */
|
||||
.delete-button {
|
||||
padding: 4px 12px;
|
||||
background: #ef5350;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.delete-button:hover {
|
||||
background: #d32f2f;
|
||||
}
|
||||
|
||||
/* Statistics Section */
|
||||
.stats-section {
|
||||
background: white;
|
||||
border: 2px solid #333;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.stats-section h3 {
|
||||
margin: 0 0 20px 0;
|
||||
color: #333;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #f8f9fa;
|
||||
border: 2px solid #dee2e6;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
border-color: #42a5f5;
|
||||
box-shadow: 0 2px 8px rgba(66, 165, 245, 0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
line-height: 1;
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,365 +0,0 @@
|
|||
<!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="breadcrumb-container">
|
||||
<div class="breadcrumb">
|
||||
<span class="breadcrumb-item">Memory Graph</span>
|
||||
<span class="breadcrumb-separator">/</span>
|
||||
<span class="breadcrumb-item">Agent:</span>
|
||||
<select id="global-agent-selector" class="agent-selector">
|
||||
<option value="">Select an agent...</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-container">
|
||||
<div class="tab-buttons">
|
||||
<button class="tab-button active" onclick="switchMainTab('data')">Data</button>
|
||||
<button class="tab-button" onclick="switchMainTab('debug')">Search Debug</button>
|
||||
<button class="tab-button" onclick="switchMainTab('think')">Think</button>
|
||||
</div>
|
||||
|
||||
<!-- Data Tab -->
|
||||
<div id="data-tab" class="tab-content active">
|
||||
<!-- Statistics Section -->
|
||||
<div id="stats-section" class="stats-section" style="display: none;">
|
||||
<h3>📊 Memory Statistics</h3>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Total Nodes</div>
|
||||
<div class="stat-value" id="stat-total-nodes">-</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">World Facts</div>
|
||||
<div class="stat-value" id="stat-world-nodes">-</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Agent Facts</div>
|
||||
<div class="stat-value" id="stat-agent-nodes">-</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Opinions</div>
|
||||
<div class="stat-value" id="stat-opinion-nodes">-</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Total Links</div>
|
||||
<div class="stat-value" id="stat-total-links">-</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Temporal Links</div>
|
||||
<div class="stat-value" id="stat-temporal-links">-</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Semantic Links</div>
|
||||
<div class="stat-value" id="stat-semantic-links">-</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Entity Links</div>
|
||||
<div class="stat-value" id="stat-entity-links">-</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Documents</div>
|
||||
<div class="stat-value" id="stat-documents">-</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="data-sub-tabs">
|
||||
<button class="data-sub-tab-button active" onclick="switchDataSubTab('world')">World</button>
|
||||
<button class="data-sub-tab-button" onclick="switchDataSubTab('agent')">Agent</button>
|
||||
<button class="data-sub-tab-button" onclick="switchDataSubTab('opinion')">Opinions</button>
|
||||
<button class="data-sub-tab-button" onclick="switchDataSubTab('documents')">Documents</button>
|
||||
</div>
|
||||
|
||||
<!-- World, Agent, and Opinions subtabs share the same structure -->
|
||||
<div id="world-subtab" class="data-subtab-content active">
|
||||
<div id="world-no-agent-message" class="no-agent-message">
|
||||
<h3>No Agent Selected</h3>
|
||||
<p>Please select an agent from the dropdown above to view world facts.</p>
|
||||
</div>
|
||||
<div id="world-content" style="display: none;">
|
||||
<div class="data-controls" style="margin-bottom: 15px; padding: 10px; background: #f5f5f5; border-radius: 4px;">
|
||||
<button onclick="loadDataView('world')" class="load-button">📊 Load World Facts</button>
|
||||
<button onclick="loadDataView('world')" class="refresh-button">🔄 Refresh</button>
|
||||
<span id="world-node-count" class="node-count"></span>
|
||||
</div>
|
||||
<div class="view-toggle">
|
||||
<button class="view-toggle-button active" onclick="switchDataView('world', 'graph')">Graph</button>
|
||||
<button class="view-toggle-button" onclick="switchDataView('world', 'table')">Table</button>
|
||||
</div>
|
||||
<div id="world-graph-view" class="data-view">
|
||||
<div class="data-controls">
|
||||
<div>
|
||||
<label>Limit nodes:</label>
|
||||
<input type="number" id="world-node-limit" value="50" min="10" max="1000" step="10" class="control-input">
|
||||
</div>
|
||||
<div>
|
||||
<label>Layout:</label>
|
||||
<select id="world-layout-select" class="control-select">
|
||||
<option value="circle">Circle (fast)</option>
|
||||
<option value="grid">Grid (fast)</option>
|
||||
<option value="cose">Force-directed (slow)</option>
|
||||
</select>
|
||||
</div>
|
||||
<button onclick="reloadDataGraph('world')" class="apply-button">Apply</button>
|
||||
</div>
|
||||
<div id="world-cy" class="graph-canvas"></div>
|
||||
<div class="legend">
|
||||
<h3>Legend</h3>
|
||||
<h4>Link Types:</h4>
|
||||
<div class="legend-item"><div class="legend-line temporal"></div><span><b>Temporal</b></span></div>
|
||||
<div class="legend-item"><div class="legend-line semantic"></div><span><b>Semantic</b></span></div>
|
||||
<div class="legend-item"><div class="legend-line entity"></div><span><b>Entity</b></span></div>
|
||||
<h4>Nodes:</h4>
|
||||
<div class="legend-item"><div class="legend-node no-entities"></div><span>No entities</span></div>
|
||||
<div class="legend-item"><div class="legend-node one-entity"></div><span>1 entity</span></div>
|
||||
<div class="legend-item"><div class="legend-node multi-entities"></div><span>2+ entities</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="world-table-view" class="data-view" style="display: none;">
|
||||
<input type="text" id="world-table-filter" placeholder="Search memories (text, context)..." class="table-filter">
|
||||
<div class="table-container">
|
||||
<table class="memory-table">
|
||||
<thead>
|
||||
<tr><th>ID</th><th>Text</th><th>Context</th><th>Date</th><th>Entities</th><th>Actions</th></tr>
|
||||
</thead>
|
||||
<tbody id="world-table-body">
|
||||
<tr><td colspan="6" class="empty-message">Click "Load World Facts" to view data</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="agent-subtab" class="data-subtab-content">
|
||||
<div id="agent-no-agent-message" class="no-agent-message">
|
||||
<h3>No Agent Selected</h3>
|
||||
<p>Please select an agent from the dropdown above to view agent facts.</p>
|
||||
</div>
|
||||
<div id="agent-content" style="display: none;">
|
||||
<div class="data-controls" style="margin-bottom: 15px; padding: 10px; background: #f5f5f5; border-radius: 4px;">
|
||||
<button onclick="loadDataView('agent')" class="load-button">📊 Load Agent Facts</button>
|
||||
<button onclick="loadDataView('agent')" class="refresh-button">🔄 Refresh</button>
|
||||
<span id="agent-node-count" class="node-count"></span>
|
||||
</div>
|
||||
<div class="view-toggle">
|
||||
<button class="view-toggle-button active" onclick="switchDataView('agent', 'graph')">Graph</button>
|
||||
<button class="view-toggle-button" onclick="switchDataView('agent', 'table')">Table</button>
|
||||
</div>
|
||||
<div id="agent-graph-view" class="data-view">
|
||||
<div class="data-controls">
|
||||
<div>
|
||||
<label>Limit nodes:</label>
|
||||
<input type="number" id="agent-node-limit" value="50" min="10" max="1000" step="10" class="control-input">
|
||||
</div>
|
||||
<div>
|
||||
<label>Layout:</label>
|
||||
<select id="agent-layout-select" class="control-select">
|
||||
<option value="circle">Circle (fast)</option>
|
||||
<option value="grid">Grid (fast)</option>
|
||||
<option value="cose">Force-directed (slow)</option>
|
||||
</select>
|
||||
</div>
|
||||
<button onclick="reloadDataGraph('agent')" class="apply-button">Apply</button>
|
||||
</div>
|
||||
<div id="agent-cy" class="graph-canvas"></div>
|
||||
<div class="legend">
|
||||
<h3>Legend</h3>
|
||||
<h4>Link Types:</h4>
|
||||
<div class="legend-item"><div class="legend-line temporal"></div><span><b>Temporal</b></span></div>
|
||||
<div class="legend-item"><div class="legend-line semantic"></div><span><b>Semantic</b></span></div>
|
||||
<div class="legend-item"><div class="legend-line entity"></div><span><b>Entity</b></span></div>
|
||||
<h4>Nodes:</h4>
|
||||
<div class="legend-item"><div class="legend-node no-entities"></div><span>No entities</span></div>
|
||||
<div class="legend-item"><div class="legend-node one-entity"></div><span>1 entity</span></div>
|
||||
<div class="legend-item"><div class="legend-node multi-entities"></div><span>2+ entities</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="agent-table-view" class="data-view" style="display: none;">
|
||||
<input type="text" id="agent-table-filter" placeholder="Search memories (text, context)..." class="table-filter">
|
||||
<div class="table-container">
|
||||
<table class="memory-table">
|
||||
<thead>
|
||||
<tr><th>ID</th><th>Text</th><th>Context</th><th>Date</th><th>Entities</th><th>Actions</th></tr>
|
||||
</thead>
|
||||
<tbody id="agent-table-body">
|
||||
<tr><td colspan="6" class="empty-message">Click "Load Agent Facts" to view data</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="opinion-subtab" class="data-subtab-content">
|
||||
<div id="opinion-no-agent-message" class="no-agent-message">
|
||||
<h3>No Agent Selected</h3>
|
||||
<p>Please select an agent from the dropdown above to view opinions.</p>
|
||||
</div>
|
||||
<div id="opinion-content" style="display: none;">
|
||||
<div class="data-controls" style="margin-bottom: 15px; padding: 10px; background: #f5f5f5; border-radius: 4px;">
|
||||
<button onclick="loadDataView('opinion')" class="load-button">📊 Load Opinions</button>
|
||||
<button onclick="loadDataView('opinion')" class="refresh-button">🔄 Refresh</button>
|
||||
<span id="opinion-node-count" class="node-count"></span>
|
||||
</div>
|
||||
<div class="view-toggle">
|
||||
<button class="view-toggle-button active" onclick="switchDataView('opinion', 'graph')">Graph</button>
|
||||
<button class="view-toggle-button" onclick="switchDataView('opinion', 'table')">Table</button>
|
||||
</div>
|
||||
<div id="opinion-graph-view" class="data-view">
|
||||
<div class="data-controls">
|
||||
<div>
|
||||
<label>Limit nodes:</label>
|
||||
<input type="number" id="opinion-node-limit" value="50" min="10" max="1000" step="10" class="control-input">
|
||||
</div>
|
||||
<div>
|
||||
<label>Layout:</label>
|
||||
<select id="opinion-layout-select" class="control-select">
|
||||
<option value="circle">Circle (fast)</option>
|
||||
<option value="grid">Grid (fast)</option>
|
||||
<option value="cose">Force-directed (slow)</option>
|
||||
</select>
|
||||
</div>
|
||||
<button onclick="reloadDataGraph('opinion')" class="apply-button">Apply</button>
|
||||
</div>
|
||||
<div id="opinion-cy" class="graph-canvas"></div>
|
||||
<div class="legend">
|
||||
<h3>Legend</h3>
|
||||
<h4>Link Types:</h4>
|
||||
<div class="legend-item"><div class="legend-line temporal"></div><span><b>Temporal</b></span></div>
|
||||
<div class="legend-item"><div class="legend-line semantic"></div><span><b>Semantic</b></span></div>
|
||||
<div class="legend-item"><div class="legend-line entity"></div><span><b>Entity</b></span></div>
|
||||
<h4>Nodes:</h4>
|
||||
<div class="legend-item"><div class="legend-node no-entities"></div><span>No entities</span></div>
|
||||
<div class="legend-item"><div class="legend-node one-entity"></div><span>1 entity</span></div>
|
||||
<div class="legend-item"><div class="legend-node multi-entities"></div><span>2+ entities</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="opinion-table-view" class="data-view" style="display: none;">
|
||||
<input type="text" id="opinion-table-filter" placeholder="Search memories (text, context)..." class="table-filter">
|
||||
<div class="table-container">
|
||||
<table class="memory-table">
|
||||
<thead>
|
||||
<tr><th>ID</th><th>Text</th><th>Context</th><th>Date</th><th>Entities</th><th>Actions</th></tr>
|
||||
</thead>
|
||||
<tbody id="opinion-table-body">
|
||||
<tr><td colspan="6" class="empty-message">Click "Load Opinions" to view data</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="documents-subtab" class="data-subtab-content">
|
||||
<div id="documents-no-agent-message" class="no-agent-message">
|
||||
<h3>No Agent Selected</h3>
|
||||
<p>Please select an agent from the dropdown above to view documents.</p>
|
||||
</div>
|
||||
<div id="documents-content" style="display: none;">
|
||||
<div class="data-controls" style="margin-bottom: 15px; padding: 10px; background: #f5f5f5; border-radius: 4px;">
|
||||
<button onclick="loadDocumentsView()" class="load-button">📄 Load Documents</button>
|
||||
<button onclick="loadDocumentsView()" class="refresh-button">🔄 Refresh</button>
|
||||
<span id="documents-count" class="node-count"></span>
|
||||
</div>
|
||||
<div class="data-view">
|
||||
<input type="text" id="documents-filter" placeholder="Search documents (ID, metadata)..." class="table-filter">
|
||||
<div class="table-container">
|
||||
<table class="memory-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Document ID</th>
|
||||
<th>Created</th>
|
||||
<th>Updated</th>
|
||||
<th>Text Length</th>
|
||||
<th>Memory Units</th>
|
||||
<th>Metadata</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="documents-table-body">
|
||||
<tr><td colspan="7" class="empty-message">Click "Load Documents" to view data</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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="think-tab" class="tab-content">
|
||||
<h2>Think - AI-Powered Answers</h2>
|
||||
<p style="color: #666; margin-bottom: 15px;">
|
||||
Ask questions and get AI-generated answers based on agent identity and world facts.
|
||||
</p>
|
||||
<div class="think-controls">
|
||||
<div style="display: flex; gap: 15px; align-items: flex-end; flex-wrap: wrap; margin-bottom: 15px;">
|
||||
<div style="flex: 1; min-width: 300px;">
|
||||
<label style="font-weight: bold; display: block; margin-bottom: 5px;">Question:</label>
|
||||
<input type="text" id="think-query" placeholder="Enter your question..." style="width: 100%; padding: 10px; border: 2px solid #ccc; border-radius: 4px; font-size: 14px;">
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-weight: bold; display: block; margin-bottom: 5px;">Budget:</label>
|
||||
<input type="number" id="think-budget" value="50" min="10" max="1000" style="width: 80px; padding: 10px; border: 2px solid #ccc; border-radius: 4px; font-size: 14px;">
|
||||
</div>
|
||||
<button id="think-button" onclick="runThink()" style="padding: 10px 24px; background: #66bb6a; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 14px;">
|
||||
💭 Think
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="think-result" style="display: none; margin-top: 20px;">
|
||||
<div class="think-answer">
|
||||
<h3 style="margin-top: 0; color: #333; border-bottom: 2px solid #333; padding-bottom: 10px;">Answer</h3>
|
||||
<div id="think-answer-text" style="padding: 15px; background: #f9f9f9; border-left: 4px solid #66bb6a; font-size: 15px; line-height: 1.6; white-space: pre-wrap;"></div>
|
||||
</div>
|
||||
<div class="think-sources" style="margin-top: 30px;">
|
||||
<h3 style="margin-top: 0; color: #333; border-bottom: 2px solid #333; padding-bottom: 10px;">Based On</h3>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 20px; margin-top: 15px;">
|
||||
<div>
|
||||
<h4 style="margin: 0 0 10px 0; color: #1976d2;">World Facts (General Knowledge)</h4>
|
||||
<div id="think-world-facts" style="background: #e3f2fd; padding: 15px; border-radius: 4px; border: 2px solid #1976d2; min-height: 100px;"></div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 style="margin: 0 0 10px 0; color: #f57c00;">Agent Facts (Identity)</h4>
|
||||
<div id="think-agent-facts" style="background: #fff3e0; padding: 15px; border-radius: 4px; border: 2px solid #f57c00; min-height: 100px;"></div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 style="margin: 0 0 10px 0; color: #7b1fa2;">Opinions (Agent Beliefs)</h4>
|
||||
<div id="think-opinions" style="background: #f3e5f5; padding: 15px; border-radius: 4px; border: 2px solid #7b1fa2; min-height: 100px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="think-new-opinions" style="display: none; margin-top: 30px;">
|
||||
<div style="background: #e8f5e9; padding: 20px; border-radius: 8px; border: 2px solid #4caf50;">
|
||||
<h3 style="margin-top: 0; color: #2e7d32; border-bottom: 2px solid #4caf50; padding-bottom: 10px;">✨ New Opinions Formed</h3>
|
||||
<div id="think-new-opinion-list" style="margin-top: 15px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="think-loading" style="display: none; text-align: center; padding: 40px; color: #666;">
|
||||
<div style="font-size: 48px; margin-bottom: 10px;">💭</div>
|
||||
<div style="font-size: 18px;">Thinking...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="./static/js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -29,6 +29,8 @@ dependencies = [
|
|||
"dateparser>=1.2.0",
|
||||
"tiktoken>=0.12.0",
|
||||
"httpx>=0.27.0",
|
||||
"streamlit>=1.51.0",
|
||||
"python-fasthtml>=0.12.33",
|
||||
]
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
|
|
|
|||
|
|
@ -8,5 +8,4 @@ echo ""
|
|||
echo "Server will be available at: http://localhost:8001"
|
||||
echo ""
|
||||
|
||||
cd benchmarks/visualizer
|
||||
uv run uvicorn server:app --reload --host 0.0.0.0 --port 8001
|
||||
uv run python benchmarks/visualizer/main.py
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ echo ""
|
|||
|
||||
# Set default arguments if not provided
|
||||
if [[ ${#SERVER_ARGS[@]} -eq 0 ]]; then
|
||||
SERVER_ARGS=(--reload --host 0.0.0.0 --port 8080)
|
||||
SERVER_ARGS=(--host 0.0.0.0 --port 8080)
|
||||
fi
|
||||
|
||||
uv run python -m memora.web.server "${SERVER_ARGS[@]}"
|
||||
|
|
|
|||
|
|
@ -19,8 +19,7 @@ async def test_document_creation_and_retrieval(memory):
|
|||
agent_id=agent_id,
|
||||
content="Alice works at Google. Bob works at Microsoft.",
|
||||
context="Team meeting",
|
||||
document_id=document_id,
|
||||
document_metadata={"source": "meeting", "participants": ["Alice", "Bob"]}
|
||||
document_id=document_id
|
||||
)
|
||||
|
||||
# Retrieve document
|
||||
|
|
@ -30,8 +29,7 @@ async def test_document_creation_and_retrieval(memory):
|
|||
assert doc["id"] == document_id
|
||||
assert doc["agent_id"] == agent_id
|
||||
assert "Alice works at Google" in doc["original_text"]
|
||||
assert doc["metadata"]["source"] == "meeting"
|
||||
assert doc["unit_count"] > 0
|
||||
assert doc["memory_unit_count"] > 0
|
||||
|
||||
finally:
|
||||
await memory.delete_agent(agent_id)
|
||||
|
|
@ -39,7 +37,7 @@ async def test_document_creation_and_retrieval(memory):
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_document_upsert(memory):
|
||||
"""Test that upsert deletes old units and creates new ones."""
|
||||
"""Test that providing the same document_id automatically upserts (deletes old units and creates new ones)."""
|
||||
agent_id = f"test_upsert_{datetime.now(timezone.utc).timestamp()}"
|
||||
|
||||
try:
|
||||
|
|
@ -55,20 +53,19 @@ async def test_document_upsert(memory):
|
|||
|
||||
# Get document stats
|
||||
doc_v1 = await memory.get_document(document_id, agent_id)
|
||||
count_v1 = doc_v1["unit_count"]
|
||||
count_v1 = doc_v1["memory_unit_count"]
|
||||
|
||||
# Upsert with different content
|
||||
# Update with different content (automatic upsert when same document_id is provided)
|
||||
units_v2 = await memory.put_async(
|
||||
agent_id=agent_id,
|
||||
content="Alice works at Microsoft. Bob works at Apple.",
|
||||
context="Updated",
|
||||
document_id=document_id,
|
||||
upsert=True
|
||||
document_id=document_id
|
||||
)
|
||||
|
||||
# Get updated document stats
|
||||
doc_v2 = await memory.get_document(document_id, agent_id)
|
||||
count_v2 = doc_v2["unit_count"]
|
||||
count_v2 = doc_v2["memory_unit_count"]
|
||||
|
||||
# Verify old units were replaced
|
||||
assert "Microsoft" in doc_v2["original_text"]
|
||||
|
|
@ -100,7 +97,7 @@ async def test_document_deletion(memory):
|
|||
# Verify it exists
|
||||
doc = await memory.get_document(document_id, agent_id)
|
||||
assert doc is not None
|
||||
assert doc["unit_count"] > 0
|
||||
assert doc["memory_unit_count"] > 0
|
||||
|
||||
# Delete document
|
||||
result = await memory.delete_document(document_id, agent_id)
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ async def test_batch_ingestion_single_call(memory):
|
|||
results, _ = await memory.search_async(
|
||||
agent_id=agent_id,
|
||||
query=question,
|
||||
fact_type="world",
|
||||
fact_type=["world"],
|
||||
thinking_budget=100,
|
||||
top_k=5,
|
||||
enable_trace=False
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ async def test_search_with_trace(memory):
|
|||
results, trace = await memory.search_async(
|
||||
agent_id=agent_id,
|
||||
query="Who works at Google?",
|
||||
fact_type="world",
|
||||
fact_type=["world"],
|
||||
thinking_budget=20,
|
||||
max_tokens=512,
|
||||
enable_trace=True,
|
||||
|
|
@ -147,7 +147,7 @@ async def test_search_without_trace(memory):
|
|||
results, trace = await memory.search_async(
|
||||
agent_id=agent_id,
|
||||
query="test",
|
||||
fact_type="world",
|
||||
fact_type=["world"],
|
||||
thinking_budget=10,
|
||||
max_tokens=512,
|
||||
enable_trace=False,
|
||||
|
|
|
|||
603
uv.lock
603
uv.lock
|
|
@ -20,6 +20,22 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/a5/32/7df1d81ec2e50fb661944a35183d87e62d3f6c6d9f8aff64a4f245226d55/alembic-1.17.1-py3-none-any.whl", hash = "sha256:cbc2386e60f89608bb63f30d2d6cc66c7aaed1fe105bd862828600e5ad167023", size = 247848 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "altair"
|
||||
version = "5.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "jinja2" },
|
||||
{ name = "jsonschema" },
|
||||
{ name = "narwhals" },
|
||||
{ name = "packaging" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.14'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/16/b1/f2969c7bdb8ad8bbdda031687defdce2c19afba2aa2c8e1d2a17f78376d8/altair-5.5.0.tar.gz", hash = "sha256:d960ebe6178c56de3855a68c47b516be38640b73fb3b5111c2a9ca90546dd73d", size = 705305 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/f3/0b6ced594e51cc95d8c1fc1640d3623770d01e4969d29c0bd09945fafefa/altair-5.5.0-py3-none-any.whl", hash = "sha256:91a310b926508d560fe0148d02a194f38b824122641ef528113d029fcd129f8c", size = 731200 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "annotated-doc"
|
||||
version = "0.0.3"
|
||||
|
|
@ -52,6 +68,92 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "apsw"
|
||||
version = "3.51.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/15/87/4ae92b55b3b4531e047808693bc7d938373a3ebbc662d55c9cef4bc36cc3/apsw-3.51.0.0.tar.gz", hash = "sha256:a578d0ab75fd888991181e2d33be93374c7be9b0cd84de0fe53f254ba8c67e5d", size = 1155850 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/77/d0b4e263b20be784eb1ee1124e4dd59f5ff47f51164bd94986f8a1617eb8/apsw-3.51.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:033085fcf2af486768d9935ad57954bf05c730eea9ad930f69cee3b9cd16b71c", size = 1994311 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/63/07e2993037cc324edefcf5bb8baf901d8552be75857301d36e6119d867ee/apsw-3.51.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6579463ec3fafc0b839dafa3265db8468ed475df9f0e682f606eef3aebd8e0db", size = 1925541 },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/b9/fbca1f86f7b9fee4df408b850ed21eb1509db2981e91f73240678ffca1d6/apsw-3.51.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:53762ab1bdb15acbfec68d860e76dfc56b2718efde0e4119161a5e58d84fad36", size = 7292529 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/cd/1e792878391d96d932055475272cf6fe26a895478e65ec2e51bb828d7461/apsw-3.51.0.0-cp311-cp311-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:47f672795891f2218f183c62db7aa78379d89be78463715b8db4d2f8e8e95ebd", size = 6976985 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/f0/fb547350596681868cc11511ca1ef54ddefa11208106552007d42fefc816/apsw-3.51.0.0-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:6324912910cf7d9e9fa8af20b48a2a47dfe0db7fa8da21f5cd021b9ac7753a9b", size = 7137514 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/1a/e941cfe06603f918a66d108e0c643ed9ef1d0a3af9f3b627ed56238ba6ad/apsw-3.51.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:3f26f17a86bf7a2b4b9b5d414a720b9c6c361fb92fbfd8fba649cc2830ca4275", size = 7275108 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/75/9f82680e26f6545f23afe0b8fbc34ec0a57af45d566f9a8c37cf4028def2/apsw-3.51.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f00d5c0505550daefaef93f145e9bed548c0e10325f01e89dbd30dcdf4e76889", size = 7247371 },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/7f/1c770f58298828e5bcdb0cdc95e249dcb975dac87ea45fcdd462d462e239/apsw-3.51.0.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:730da007cf0f8ed3904e444c75646d46130a2fbb59f27b3f00a3024b2a4910a8", size = 7121158 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/6b/5aa4773dab1a1a208c11dc66bf9d5a94887ba7c7c7dd11ab74612aed5889/apsw-3.51.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5a43412fc7e7ae2d2ad275042ab344c8b9f8f0bb5b56c6b17c3f6a81b2aa23bf", size = 7203311 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/3d/bbf1e7829c0ffe543d84b116c952b82fc9a2bbde080b9e0f2f102527f1cd/apsw-3.51.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:78f18c1c30c39b687bf951c33b1b6ea791f63b178bbfadac4bfdf74c2f37ed5e", size = 7261775 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/13/f4fb66a8d114f7be799c63cbb01dcd2f6fa0c9c880718df53e32b14da8af/apsw-3.51.0.0-cp311-cp311-win32.whl", hash = "sha256:6fde4a4a914ba6ed096fca31d48a368dd13e0d7ed65d12016af21163d5dccb50", size = 1620943 },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/34/660058bfb96aefda65471b356b77ee32fa9b2fb50166d8387e52b77520a1/apsw-3.51.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:40e5307d3e1bcde5e0d53043326e500dcbe7ff10fdfc07436ea91cff09fb8aa1", size = 1814138 },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/8d/0123908b1f290393739fff5e2b8d8ceb207f54773a92b78c42090899b77d/apsw-3.51.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:1b64b0aa47834b9471ea77a340ca795008894b35a5c5df5186c5686fcf167b43", size = 1637616 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/2a/be0e2887cfd3d6a5ca50f97936623241890ba71bcee57b4bf60e9fbb8de7/apsw-3.51.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dd9566609035adc640fa26d1cf84d4b4a1e0bd581242d7f8633688eb5ef0af03", size = 1995037 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/27/23c08d2c68dad4e35048773fb6e8f958e2ca3e8053c60ee425c9e4399cad/apsw-3.51.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:faa812ff83e6a2b0961cf2e85b58f6f84e668921bdce29b1ab02175ebb0b4d56", size = 1925119 },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/de/f771919c3d11b6cebb7fb496ff2d8c6c373f690d5439adcbd85b008e1764/apsw-3.51.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:a92b9bf7f3fbf69844e4a3abf1cc8765279bd9418c643ca618f7a141c2490d98", size = 7292669 },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/94/1f9388800587b21fd8804bcef013f17118dc37044bcdd90fe0d28d7febe2/apsw-3.51.0.0-cp312-cp312-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:27b13870ffeac60c23e56c0f1ed0597334d82bd86eac43551d7fa42b84a07b7b", size = 6958595 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/45/367d3250a763c37f80d3d6ef29adc318c343b39ee2b6159db0679cad7f28/apsw-3.51.0.0-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:803a11256843b4f034f4853ede74cd805d1a0b465b230af61b33032579bf7eed", size = 7128158 },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/6a/734cd74c61ed8f9d3183c180903cea9182b195484e96d60cefc60e41804a/apsw-3.51.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:777d1ae6befb71b91142c8f9c5333c260b1fe3862ee418bcd18edc5bfe78efcb", size = 7278772 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/e0/16e6253250f9a51074e99426da77a039856595674f6ad0c548ef1dab9bd2/apsw-3.51.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8ee7927b46432097a5c2338e3fdbb5d8fb4754468af98bc47237565e2deee71a", size = 7239538 },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/59/0b6a31f2179f5aceb79397da44b64997c5b71c47857583fe022204f06295/apsw-3.51.0.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:553da4f587c9bde15936fafdb969d1cc724e30b9ca5c33a884bcfa53a3032eac", size = 7118313 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/4f/2ce0772637930de03aeee1eff4a8421e2f3b868409fa1242cc9fe5045cb5/apsw-3.51.0.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ee44fe4eae72cd1599dd229a886969da8c4d06eb3b208827e3ea26a01882958a", size = 7186757 },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/38/23431da004add445138434aad11b15a4163b60e1848132469fafe5d81855/apsw-3.51.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0091810cb090348aab59456eaf233b7aa664d48235001717ee88b0bbfb772d78", size = 7261969 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/30/31a57edc3295c608316630adff804a010e0da0865f48d9eae6e895d380eb/apsw-3.51.0.0-cp312-cp312-win32.whl", hash = "sha256:4695f90281e6b8fa4986c7a220187374c1a53d740df8eea4410e69b932e90c6c", size = 1621212 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/43/fd1ea94ebf6191235cb03825d024fec29c9e7fdf79f2b1c13482c7a6fa4e/apsw-3.51.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:85a1e55824cc61015d1c0550423878f5824531d8424be05a326c7a63f62ee2dd", size = 1813295 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/59/c2608c295ad532dfc953475417ab6fa3e71831e725d3d0f95d3647473cf9/apsw-3.51.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:b4b23e9f4c67bda707bdbf9e3ff69d02ec35f554fb72b72e6988b9427ffd8239", size = 1636994 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/77/6560bdf8ec6ee65b04b104eb5529ce4607abad1f4bce7560fa14cecd23da/apsw-3.51.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9b2ce15aea77c63a02b5c6b5a72cf8111526648e79d81217ddb93174e69cf1d", size = 1993527 },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/61/64221f6a61be5dc991ad833944d5467b7a6558d084c951250517ad6b868d/apsw-3.51.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:58c1c012fd3bb0dca18cac87989599b873652ef5af75087cf4ce62c4df1e696d", size = 1922853 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/13/db2e95c3668a9dd90b41adcff62f314e3b1091eeabe33d3c4c98ab508685/apsw-3.51.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:cc1ab42b5dafe5395161ac0d4b4a8f8ea0ad253c0ac4dfd5fe698091e7115a54", size = 7302903 },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/39/71b006e1cc39408c15d09ae8af9c943624c926d47aa5b07610e338dbbf29/apsw-3.51.0.0-cp313-cp313-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b1b18db4a0e45fb9af9ec72a47fce2a8f03b39ee94d5ba4687cf85ef9c51956", size = 6977749 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/a2/a04019066e6a96a837e8ff8f4c962ab578a7982f2aab15ecf39e73df8ecd/apsw-3.51.0.0-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:86489b39c4b95784c19a928b66ea2e376e91fdbf3a41192fa28c5bd266eb1ac4", size = 7133132 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/03/e22061fd1982f96a719b3659a58df87c36c882771d4bd07b4dab495cb57e/apsw-3.51.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5af457e56fad0b3387cda6b6da389fc924a169978a84553fa2ffad504d3f9f5d", size = 7301008 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/5b/288817a139200cec1554ea6d395f561ca491f02dc1c9c7a82fe537edf37f/apsw-3.51.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9aed80d52bc01efc5b278d63e02672d9f65f7faa8a560f931c2e69c392963330", size = 7240049 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/68/f6a83181f16abfb32ebeef646cfe4d02a11ea551775697f6eb9fa47ec5b5/apsw-3.51.0.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9870c968a83ba863f8b8bf63dd24c3922dc9ef16b072fe2e821d814120b3e0b8", size = 7128689 },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/66/e25c28881e2e69811ea39d16e42282235a459111727c3c80c9df9997016e/apsw-3.51.0.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4f4f04225f0fafe0aaa5123f7479cacc48db40d10ebbfe35639e4afaab3b5188", size = 7197057 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/84/e52c87b14d6ef0c948f618cc5c8f068f795ea4e02e4f52bd7065d2a2b8f9/apsw-3.51.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b3562f2586fc572e033a66a212dfbd372e1494d84de507f81e8c6935e82b1c0d", size = 7283033 },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/93/760f3de5f5da06677b668432887c664c28abac2aa50a6ea962db0aa4f05f/apsw-3.51.0.0-cp313-cp313-win32.whl", hash = "sha256:8c9de89cba374d7816a6da75cc7683f2517340f24ed8fcebea7a8a92d1ab0ab0", size = 1620246 },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/ad/0d593cafaae7098ca9bd90500966ef985da36d881a6561d4fe5ab6eae1cb/apsw-3.51.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:5ed2b442c2e2f54fcb5e51229ab19aa7ab72cde70b35075434b99c3d5c0e7310", size = 1811893 },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/16/06e1813750f483e20fffb0f8d3bcd56403c6b5dd97e0ccef5adc502cbedd/apsw-3.51.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:ef90574d4fa462ae6cb4bee63ce5bf838619b04f9493552bcd0834a8038f3423", size = 1636226 },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/c7/8817bc43aa1fff547a161c8bdf40aaf1e673275ea9f9e985a5f85a9a6c9e/apsw-3.51.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7275007d44a95ee767aad4e1b60b7c188c6dc239555167cd051dbdf73bcdb17b", size = 1993660 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/69/797d7e875c7eeabd6ccd89323a088d1829017abd0b9780744f0a8a2fcea2/apsw-3.51.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:73008bb040d36930d84313aad2b8ce2b60b98c3f0aaa7daf5d9c74057e81f17a", size = 1923105 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/54/76f65b3848b5308d1a8f943b7289781a1465acb3da67ec411b2ce349b87d/apsw-3.51.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:159134b6528fd640eab5684407f03f1f114dc69b21b34f5760e22c9a15e2d2a0", size = 7302762 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/c0/75e35ff54f03a7dacfe7c4c9ef9bbf7a3e11eb99c4db56ba4a7f0fc80a56/apsw-3.51.0.0-cp314-cp314-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c54de8e369298c6827bc5019319834923a0220aa673c22f248bfad126d8ccde4", size = 6973177 },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/2a/8f3abb11e58d20b5b8b276cf8bfe5e7b914815f513665b196e90adf5cb6d/apsw-3.51.0.0-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:11fd4f9d4137358ed5c37f57778f25cc0e24545cc797f815f3b310c478f35246", size = 7133090 },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/20/ee07bf22cf9363a2e1d01eda2695e7b960ed0385a2528c995785afd922f5/apsw-3.51.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:0e85a5f7f8144e788ff41dcc4967fea0341bca30aaede4d294504857015d6f47", size = 7297907 },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/17/d0fea69c39dc85703ee2972ca797d62c897fd7b041e77ab35b6ba50f5cbc/apsw-3.51.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b09e5a110acdb5890f356a9cb2b6e18fb8e58d0a4fc010af93ca613719bb604f", size = 7240256 },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/97/b1ca3ffc30d8a557cd6f2b01c8997bb5f464051aa84624741f45c1ff8b0e/apsw-3.51.0.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:fdbc9518c242db5c83189e915aa4d302bcc7b5b6fa4ba50c6c2f71e980156263", size = 7117788 },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/79/78d14eeb7b0d05a42c8a1c29354af84299b81b35e41390785eb4c7ba8521/apsw-3.51.0.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad6592c64ad06780a98a12d85f3594aa6dd5f95fbbd9477cfee5962dae87f089", size = 7197290 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/ab/be9481fe0e4288ca553e2239fcb8ba70e49db5f2e6c12b7158586b01e1ef/apsw-3.51.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:42b77f3baae0f734ebee014a74fa8c798343c69027f00f5769642f6674df4248", size = 7280000 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/d5/8a0e56bbd47965ccfc22b1a5776ffa29b43709bb912b0636af889c9100ad/apsw-3.51.0.0-cp314-cp314-win32.whl", hash = "sha256:3303488ef66a43a75fc6d7c219aa9cb6bdc5733ffdf2d620e32f3afb7d853e5d", size = 1653439 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/7f/6d1af2195ba102f55f1e738823c2fede579e8e130b5a28320b7239ce6841/apsw-3.51.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:1b503bdfd2294082ee0aa548f046c7bc36ceb7688723474a391758504f710f2e", size = 1855706 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/f0/00a84080dc18cf2bc2644fb62f143bbeba8ad993e528590ae07723dd953b/apsw-3.51.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:9f586ebbdbad094884dd62e2ea331f1541d98d1fdbb81079dfa605071dac8c65", size = 1676424 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/9d/f3dc47f980728b1116b60173b3db9f6b3c32c1d27a09feafe7f98e2160c7/apsw-3.51.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c2c8b0d4e27e3fc4f4a7dced8522ad4929e8ef4757531287e5e5056d39dc4d0e", size = 1999344 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/ff/bced0acae1a2d3ccb5d6183768e6cd4270c8414b14e3ddc752f321a71284/apsw-3.51.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4bb6d2fd8b6e024403fa7cdfe674b5024fedc806c06154944e1bd29b2f3126f9", size = 1930308 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/67/dbc5952d769345bbeccebf86ee2abf1f95c1a885b80b35d72504dfc4c84d/apsw-3.51.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:1a48442ec88b86f80471cf75e00d7b31b84066aa0e18b711d732c1599bd7d86b", size = 7301498 },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/ed/52edcdf1563e330f89c91bfe55e5e90e096394385468cdf5263aea620c16/apsw-3.51.0.0-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ad344b8c66449b5d02e611d6627feeaa631cb40ebcc7b790a97c78fa4c8e6923", size = 6957832 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/13/4c1a029a3691155d38e3dc9fdcc4ec29c98b9945974e9149e0bc5f35465a/apsw-3.51.0.0-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:aedda92be24ae67a601024bad63ff72964a3276db6e2c13245667f3692e52c28", size = 7110634 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/48/39a34654117c573c2dea294b72d35a8dd170db5971c56ae5c52040619990/apsw-3.51.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c49da8ca4f56c5f95b699a0e13b30611525a67196f447132fb3bca0298748bf6", size = 7279930 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/30/9b5aaa26bc070280701777be8324d28706ca7834bcbb7a8df98c1d0cc813/apsw-3.51.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:065655d25a92c12b743925a2a4a22673c1c75df6c4ebc856c4f817287ba85149", size = 7242950 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/1a/0bccc59855e10da233e7c25864129dc32761f4a03ee85e72a7e8ae845eee/apsw-3.51.0.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:41162a12def892024c27a7ab6092c2e73a2e7e5afaad82ecabacdca5923cde55", size = 7083261 },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/14/1d488695548c387e15f4d283328115f6ec45349dd26755f32155f2f8a1d7/apsw-3.51.0.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:268c728584be5b8a207f81250ae4428990a2be34468a0f59e37adc71b4994ab6", size = 7154292 },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/39/69cb76d7db133f31c1a4bf940d2c6028af139f58dc22275b3015a71f3b25/apsw-3.51.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8433ccefa7fe13b46b8fedce5399353e024514246abf345dce091e120bf5ddfd", size = 7246897 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/99/cec76fce4e472e8571a16bcd619fd1437495840805ace7fc4dd0eb066ae6/apsw-3.51.0.0-cp314-cp314t-win32.whl", hash = "sha256:6e4818d74724c980fa372484b26accb227957e6bc087919aa355e071cfa24720", size = 1666312 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/ab/7435c50bbc4ab2699e9cf0dc1f0261d89e8139c892e7103502e66288c92e/apsw-3.51.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e3cb6933062ee81a1d9b797ef5f43c9e5a4cf10a0e2f5568a9efc1f29d589878", size = 1871560 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/e8/93d56a912d6bcc629e8d9f5b6088c4dcd93ceba18574f557fe010dadf665/apsw-3.51.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:ac6d80752b3fe1e43b90f2dca92f30cc421e462f7d37c4a71c40fd6c86a525e8", size = 1680428 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "apswutils"
|
||||
version = "0.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "apsw" },
|
||||
{ name = "fastcore" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ad/62/ce0e99df611dfffda43fd77e6ea89db952eff2a31b40bb152c2e42987773/apswutils-0.1.0.tar.gz", hash = "sha256:4c576b259d708a0f72cdced92565e4f78309913d2082dbe3c9bf653142ddbe58", size = 51051 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/44/9f/bb0f0a1dcce6f478c8d16f10d283a6f5d95cbc3ebc810538f80fee605a81/apswutils-0.1.0-py3-none-any.whl", hash = "sha256:a39ace8a9f14a9bf367993acaa5a867fa57d365f975319983dace3a258f95843", size = 80509 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asyncpg"
|
||||
version = "0.30.0"
|
||||
|
|
@ -84,6 +186,46 @@ 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 = "attrs"
|
||||
version = "25.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "beautifulsoup4"
|
||||
version = "4.14.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "soupsieve" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/77/e9/df2358efd7659577435e2177bfa69cba6c33216681af51a707193dec162a/beautifulsoup4-4.14.2.tar.gz", hash = "sha256:2a98ab9f944a11acee9cc848508ec28d9228abfd522ef0fad6a02a72e0ded69e", size = 625822 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/94/fe/3aed5d0be4d404d12d36ab97e2f1791424d9ca39c2f754a6285d59a3b01d/beautifulsoup4-4.14.2-py3-none-any.whl", hash = "sha256:5ef6fa3a8cbece8488d66985560f97ed091e22bbc4e9c2338508a9d5de6d4515", size = 106392 },
|
||||
]
|
||||
|
||||
[[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 = "cachetools"
|
||||
version = "6.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cc/7e/b975b5814bd36faf009faebe22c1072a1fa1168db34d285ef0ba071ad78c/cachetools-6.2.1.tar.gz", hash = "sha256:3f391e4bd8f8bf0931169baf7456cc822705f4e2a31f840d218f445b9a854201", size = 31325 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/96/c5/1e741d26306c42e2bf6ab740b2202872727e0f606033c9dd713f8b93f5a8/cachetools-6.2.1-py3-none-any.whl", hash = "sha256:09868944b6dde876dfd44e1d47e18484541eaf12f26f29b7af91b26cc892d701", size = 11280 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.10.5"
|
||||
|
|
@ -296,6 +438,31 @@ 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 = "fastcore"
|
||||
version = "1.8.16"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "packaging" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/08/46/c581a997957c30a71f18fddbe79b37968b6d68a9d91f0a0c86b3ecd0adcd/fastcore-1.8.16.tar.gz", hash = "sha256:0d8d5c11d88b36fbc4728a341ffcedcfc62220fbe5b14b442bccb512043d84c9", size = 83669 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/27/f4/30077733807ca57cc504a6faadcdd067714a7891dad66231b0c1ab373551/fastcore-1.8.16-py3-none-any.whl", hash = "sha256:ce49c2580cfcf416a7ee166f9d84f61a828beb6be1315635c6035ec3f626ed4b", size = 86810 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastlite"
|
||||
version = "0.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "apswutils" },
|
||||
{ name = "fastcore" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3b/df/0dc6b64fd9ea7356c30eac8ac6e3c77b1068de095df9f37d579b5fe71943/fastlite-0.2.1.tar.gz", hash = "sha256:0b8f0bea62d197175beeff2719086dd361b5c418fb3e52c485cfb99b918925c2", size = 22348 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/86/1d/f09c2994a7e1c9c32dc0453b6fd7d4ae7830d26a17fd4f1e014b10b999ed/fastlite-0.2.1-py3-none-any.whl", hash = "sha256:106488269de593b41fed7e828dd2dc74dafc67ca5f6dc974c5c53f74d7b30be4", size = 17509 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filelock"
|
||||
version = "3.20.0"
|
||||
|
|
@ -314,6 +481,30 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/47/71/70db47e4f6ce3e5c37a607355f80da8860a33226be640226ac52cb05ef2e/fsspec-2025.9.0-py3-none-any.whl", hash = "sha256:530dc2a2af60a414a832059574df4a6e10cce927f6f4a78209390fe38955cfb7", size = 199289 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gitdb"
|
||||
version = "4.0.12"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "smmap" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gitpython"
|
||||
version = "3.1.45"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "gitdb" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9a/c8/dd58967d119baab745caec2f9d853297cec1989ec1d63f677d3880632b88/gitpython-3.1.45.tar.gz", hash = "sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c", size = 215076 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77", size = 208168 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "greenlet"
|
||||
version = "3.2.4"
|
||||
|
|
@ -503,6 +694,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"
|
||||
|
|
@ -630,6 +830,33 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonschema"
|
||||
version = "4.25.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "attrs" },
|
||||
{ name = "jsonschema-specifications" },
|
||||
{ name = "referencing" },
|
||||
{ name = "rpds-py" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonschema-specifications"
|
||||
version = "2025.9.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "referencing" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "langchain-core"
|
||||
version = "1.0.2"
|
||||
|
|
@ -805,9 +1032,11 @@ dependencies = [
|
|||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-timeout" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "python-fasthtml" },
|
||||
{ name = "rich" },
|
||||
{ name = "sentence-transformers" },
|
||||
{ name = "sqlalchemy" },
|
||||
{ name = "streamlit" },
|
||||
{ name = "tiktoken" },
|
||||
{ name = "uvicorn" },
|
||||
]
|
||||
|
|
@ -829,9 +1058,11 @@ requires-dist = [
|
|||
{ name = "pytest-asyncio", specifier = ">=0.21.0" },
|
||||
{ name = "pytest-timeout", specifier = ">=2.4.0" },
|
||||
{ name = "python-dotenv", specifier = ">=1.0.0" },
|
||||
{ name = "python-fasthtml", specifier = ">=0.12.33" },
|
||||
{ name = "rich", specifier = ">=13.0.0" },
|
||||
{ name = "sentence-transformers", specifier = ">=2.2.0" },
|
||||
{ name = "sqlalchemy", specifier = ">=2.0.44" },
|
||||
{ name = "streamlit", specifier = ">=1.51.0" },
|
||||
{ name = "tiktoken", specifier = ">=0.12.0" },
|
||||
{ name = "uvicorn", specifier = ">=0.38.0" },
|
||||
]
|
||||
|
|
@ -845,6 +1076,15 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "narwhals"
|
||||
version = "2.10.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c5/dc/8db74daf8c2690ec696c1d772a33cc01511559ee8a9e92d7ed85a18e3c22/narwhals-2.10.2.tar.gz", hash = "sha256:ff738a08bc993cbb792266bec15346c1d85cc68fdfe82a23283c3713f78bd354", size = 584954 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/47/a9/9e02fa97e421a355fc5e818e9c488080fce04a8e0eebb3ed75a84f041c4a/narwhals-2.10.2-py3-none-any.whl", hash = "sha256:059cd5c6751161b97baedcaf17a514c972af6a70f36a89af17de1a0caf519c43", size = 419573 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "networkx"
|
||||
version = "3.5"
|
||||
|
|
@ -1069,6 +1309,15 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/a2/eb/86626c1bbc2edb86323022371c39aa48df6fd8b0a1647bc274577f72e90b/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f", size = 89954 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oauthlib"
|
||||
version = "3.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openai"
|
||||
version = "2.6.1"
|
||||
|
|
@ -1165,6 +1414,60 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pandas"
|
||||
version = "2.3.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "numpy" },
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "pytz" },
|
||||
{ name = "tzdata" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453 },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618 },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002 },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971 },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671 },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807 },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872 },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371 },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056 },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189 },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912 },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233 },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635 },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049 },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071 },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582 },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963 },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pgvector"
|
||||
version = "0.4.1"
|
||||
|
|
@ -1273,6 +1576,21 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "protobuf"
|
||||
version = "6.33.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/19/ff/64a6c8f420818bb873713988ca5492cba3a7946be57e027ac63495157d97/protobuf-6.33.0.tar.gz", hash = "sha256:140303d5c8d2037730c548f8c7b93b20bb1dc301be280c378b82b8894589c954", size = 443463 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/ee/52b3fa8feb6db4a833dfea4943e175ce645144532e8a90f72571ad85df4e/protobuf-6.33.0-cp310-abi3-win32.whl", hash = "sha256:d6101ded078042a8f17959eccd9236fb7a9ca20d3b0098bbcb91533a5680d035", size = 425593 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/c6/7a465f1825872c55e0341ff4a80198743f73b69ce5d43ab18043699d1d81/protobuf-6.33.0-cp310-abi3-win_amd64.whl", hash = "sha256:9a031d10f703f03768f2743a1c403af050b6ae1f3480e9c140f39c45f81b13ee", size = 436882 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/a9/b6eee662a6951b9c3640e8e452ab3e09f117d99fc10baa32d1581a0d4099/protobuf-6.33.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:905b07a65f1a4b72412314082c7dbfae91a9e8b68a0cc1577515f8df58ecf455", size = 427521 },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/35/16d31e0f92c6d2f0e77c2a3ba93185130ea13053dd16200a57434c882f2b/protobuf-6.33.0-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e0697ece353e6239b90ee43a9231318302ad8353c70e6e45499fa52396debf90", size = 324445 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/eb/2a981a13e35cda8b75b5585aaffae2eb904f8f351bdd3870769692acbd8a/protobuf-6.33.0-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:e0a1715e4f27355afd9570f3ea369735afc853a6c3951a6afe1f80d8569ad298", size = 339159 },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/51/0b1cbad62074439b867b4e04cc09b93f6699d78fd191bed2bbb44562e077/protobuf-6.33.0-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:35be49fd3f4fefa4e6e2aacc35e8b837d6703c37a2168a55ac21e9b1bc7559ef", size = 323172 },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/d1/0a28c21707807c6aacd5dc9c3704b2aa1effbf37adebd8caeaf68b17a636/protobuf-6.33.0-py3-none-any.whl", hash = "sha256:25c9e1963c6734448ea2d308cfa610e692b801304ba0908d7bfa564ac5132995", size = 170477 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psycopg2-binary"
|
||||
version = "2.9.11"
|
||||
|
|
@ -1325,6 +1643,42 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyarrow"
|
||||
version = "21.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ef/c2/ea068b8f00905c06329a3dfcd40d0fcc2b7d0f2e355bdb25b65e0a0e4cd4/pyarrow-21.0.0.tar.gz", hash = "sha256:5051f2dccf0e283ff56335760cbc8622cf52264d67e359d5569541ac11b6d5bc", size = 1133487 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/94/dc/80564a3071a57c20b7c32575e4a0120e8a330ef487c319b122942d665960/pyarrow-21.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:c077f48aab61738c237802836fc3844f85409a46015635198761b0d6a688f87b", size = 31243234 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/cc/3b51cb2db26fe535d14f74cab4c79b191ed9a8cd4cbba45e2379b5ca2746/pyarrow-21.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:689f448066781856237eca8d1975b98cace19b8dd2ab6145bf49475478bcaa10", size = 32714370 },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/11/a4431f36d5ad7d83b87146f515c063e4d07ef0b7240876ddb885e6b44f2e/pyarrow-21.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:479ee41399fcddc46159a551705b89c05f11e8b8cb8e968f7fec64f62d91985e", size = 41135424 },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/dc/035d54638fc5d2971cbf1e987ccd45f1091c83bcf747281cf6cc25e72c88/pyarrow-21.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:40ebfcb54a4f11bcde86bc586cbd0272bac0d516cfa539c799c2453768477569", size = 42823810 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/3b/89fced102448a9e3e0d4dded1f37fa3ce4700f02cdb8665457fcc8015f5b/pyarrow-21.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8d58d8497814274d3d20214fbb24abcad2f7e351474357d552a8d53bce70c70e", size = 43391538 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/bb/ea7f1bd08978d39debd3b23611c293f64a642557e8141c80635d501e6d53/pyarrow-21.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:585e7224f21124dd57836b1530ac8f2df2afc43c861d7bf3d58a4870c42ae36c", size = 45120056 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/0b/77ea0600009842b30ceebc3337639a7380cd946061b620ac1a2f3cb541e2/pyarrow-21.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:555ca6935b2cbca2c0e932bedd853e9bc523098c39636de9ad4693b5b1df86d6", size = 26220568 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/d4/d4f817b21aacc30195cf6a46ba041dd1be827efa4a623cc8bf39a1c2a0c0/pyarrow-21.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:3a302f0e0963db37e0a24a70c56cf91a4faa0bca51c23812279ca2e23481fccd", size = 31160305 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/9c/dcd38ce6e4b4d9a19e1d36914cb8e2b1da4e6003dd075474c4cfcdfe0601/pyarrow-21.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:b6b27cf01e243871390474a211a7922bfbe3bda21e39bc9160daf0da3fe48876", size = 32684264 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/74/2a2d9f8d7a59b639523454bec12dba35ae3d0a07d8ab529dc0809f74b23c/pyarrow-21.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:e72a8ec6b868e258a2cd2672d91f2860ad532d590ce94cdf7d5e7ec674ccf03d", size = 41108099 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/90/2660332eeb31303c13b653ea566a9918484b6e4d6b9d2d46879a33ab0622/pyarrow-21.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b7ae0bbdc8c6674259b25bef5d2a1d6af5d39d7200c819cf99e07f7dfef1c51e", size = 42829529 },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/27/1a93a25c92717f6aa0fca06eb4700860577d016cd3ae51aad0e0488ac899/pyarrow-21.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:58c30a1729f82d201627c173d91bd431db88ea74dcaa3885855bc6203e433b82", size = 43367883 },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/d9/4d09d919f35d599bc05c6950095e358c3e15148ead26292dfca1fb659b0c/pyarrow-21.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:072116f65604b822a7f22945a7a6e581cfa28e3454fdcc6939d4ff6090126623", size = 45133802 },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/30/f3795b6e192c3ab881325ffe172e526499eb3780e306a15103a2764916a2/pyarrow-21.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:cf56ec8b0a5c8c9d7021d6fd754e688104f9ebebf1bf4449613c9531f5346a18", size = 26203175 },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/ca/c7eaa8e62db8fb37ce942b1ea0c6d7abfe3786ca193957afa25e71b81b66/pyarrow-21.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e99310a4ebd4479bcd1964dff9e14af33746300cb014aa4a3781738ac63baf4a", size = 31154306 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/e8/e87d9e3b2489302b3a1aea709aaca4b781c5252fcb812a17ab6275a9a484/pyarrow-21.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:d2fe8e7f3ce329a71b7ddd7498b3cfac0eeb200c2789bd840234f0dc271a8efe", size = 32680622 },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/52/79095d73a742aa0aba370c7942b1b655f598069489ab387fe47261a849e1/pyarrow-21.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:f522e5709379d72fb3da7785aa489ff0bb87448a9dc5a75f45763a795a089ebd", size = 41104094 },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/4b/7782438b551dbb0468892a276b8c789b8bbdb25ea5c5eb27faadd753e037/pyarrow-21.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:69cbbdf0631396e9925e048cfa5bce4e8c3d3b41562bbd70c685a8eb53a91e61", size = 42825576 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/62/0f29de6e0a1e33518dec92c65be0351d32d7ca351e51ec5f4f837a9aab91/pyarrow-21.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:731c7022587006b755d0bdb27626a1a3bb004bb56b11fb30d98b6c1b4718579d", size = 43368342 },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/c7/0fa1f3f29cf75f339768cc698c8ad4ddd2481c1742e9741459911c9ac477/pyarrow-21.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc56bc708f2d8ac71bd1dcb927e458c93cec10b98eb4120206a4091db7b67b99", size = 45131218 },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/63/581f2076465e67b23bc5a37d4a2abff8362d389d29d8105832e82c9c811c/pyarrow-21.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:186aa00bca62139f75b7de8420f745f2af12941595bbbfa7ed3870ff63e25636", size = 26087551 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/ab/357d0d9648bb8241ee7348e564f2479d206ebe6e1c47ac5027c2e31ecd39/pyarrow-21.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:a7a102574faa3f421141a64c10216e078df467ab9576684d5cd696952546e2da", size = 31290064 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/8a/5685d62a990e4cac2043fc76b4661bf38d06efed55cf45a334b455bd2759/pyarrow-21.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:1e005378c4a2c6db3ada3ad4c217b381f6c886f0a80d6a316fe586b90f77efd7", size = 32727837 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/de/c0828ee09525c2bafefd3e736a248ebe764d07d0fd762d4f0929dbc516c9/pyarrow-21.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:65f8e85f79031449ec8706b74504a316805217b35b6099155dd7e227eef0d4b6", size = 41014158 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/26/a2865c420c50b7a3748320b614f3484bfcde8347b2639b2b903b21ce6a72/pyarrow-21.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:3a81486adc665c7eb1a2bde0224cfca6ceaba344a82a971ef059678417880eb8", size = 42667885 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/f9/4ee798dc902533159250fb4321267730bc0a107d8c6889e07c3add4fe3a5/pyarrow-21.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fc0d2f88b81dcf3ccf9a6ae17f89183762c8a94a5bdcfa09e05cfe413acf0503", size = 43276625 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/da/e02544d6997037a4b0d22d8e5f66bc9315c3671371a8b18c79ade1cefe14/pyarrow-21.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6299449adf89df38537837487a4f8d3bd91ec94354fdd2a7d30bc11c48ef6e79", size = 44951890 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/4e/519c1bc1876625fe6b71e9a28287c43ec2f20f73c658b9ae1d485c0c206e/pyarrow-21.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:222c39e2c70113543982c6b34f3077962b44fca38c0bd9e68bb6781534425c10", size = 26371006 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.12.3"
|
||||
|
|
@ -1438,6 +1792,19 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/48/f7/925f65d930802e3ea2eb4d5afa4cb8730c8dc0d2cb89a59dc4ed2fcb2d74/pydantic_core-2.41.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c173ddcd86afd2535e2b695217e82191580663a1d1928239f877f5a1649ef39f", size = 2147775 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydeck"
|
||||
version = "0.9.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "jinja2" },
|
||||
{ name = "numpy" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/ca/40e14e196864a0f61a92abb14d09b3d3da98f94ccb03b49cf51688140dab/pydeck-0.9.1.tar.gz", hash = "sha256:f74475ae637951d63f2ee58326757f8d4f9cd9f2a457cf42950715003e2cb605", size = 3832240 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/4c/b888e6cf58bd9db9c93f40d1c6be8283ff49d88919231afe93a6bcf61626/pydeck-0.9.1-py2.py3-none-any.whl", hash = "sha256:b3f75ba0d273fc917094fa61224f3f6076ca8752b93d46faf3bcfd9f9d59b038", size = 6900403 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.2"
|
||||
|
|
@ -1509,6 +1876,27 @@ 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-fasthtml"
|
||||
version = "0.12.33"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "beautifulsoup4" },
|
||||
{ name = "fastcore" },
|
||||
{ name = "fastlite" },
|
||||
{ name = "httpx" },
|
||||
{ name = "itsdangerous" },
|
||||
{ name = "oauthlib" },
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "python-multipart" },
|
||||
{ name = "starlette" },
|
||||
{ name = "uvicorn", extra = ["standard"] },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c9/54/17f98a354d2dbc33f0ea6707707ddfa48d42785f51a7ce57eb6917748c76/python_fasthtml-0.12.33.tar.gz", hash = "sha256:13fb06cf75d9ff13f6aa809324249c8b5387f5eec8ccb7a756606f23521345f9", size = 70086 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/da/5a/021d010755b3dc88a79ba847cb36e07eff9c31fe26be76e86bcbda8c16af/python_fasthtml-0.12.33-py3-none-any.whl", hash = "sha256:94900f3572fb4539625a5f1d4cf2ed55cb3a1d80eac5cfb53a6d05abffafd4a8", size = 72518 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-multipart"
|
||||
version = "0.0.20"
|
||||
|
|
@ -1582,6 +1970,20 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "referencing"
|
||||
version = "0.37.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "attrs" },
|
||||
{ name = "rpds-py" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "2025.10.23"
|
||||
|
|
@ -1805,6 +2207,114 @@ wheels = [
|
|||
{ 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 = "rpds-py"
|
||||
version = "0.28.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/48/dc/95f074d43452b3ef5d06276696ece4b3b5d696e7c9ad7173c54b1390cd70/rpds_py-0.28.0.tar.gz", hash = "sha256:abd4df20485a0983e2ca334a216249b6186d6e3c1627e106651943dbdb791aea", size = 27419 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/34/058d0db5471c6be7bef82487ad5021ff8d1d1d27794be8730aad938649cf/rpds_py-0.28.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:03065002fd2e287725d95fbc69688e0c6daf6c6314ba38bdbaa3895418e09296", size = 362344 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/67/9503f0ec8c055a0782880f300c50a2b8e5e72eb1f94dfc2053da527444dd/rpds_py-0.28.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28ea02215f262b6d078daec0b45344c89e161eab9526b0d898221d96fdda5f27", size = 348440 },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/2e/94223ee9b32332a41d75b6f94b37b4ce3e93878a556fc5f152cbd856a81f/rpds_py-0.28.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25dbade8fbf30bcc551cb352376c0ad64b067e4fc56f90e22ba70c3ce205988c", size = 379068 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/25/54fd48f9f680cfc44e6a7f39a5fadf1d4a4a1fd0848076af4a43e79f998c/rpds_py-0.28.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c03002f54cc855860bfdc3442928ffdca9081e73b5b382ed0b9e8efe6e5e205", size = 390518 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/85/ac258c9c27f2ccb1bd5d0697e53a82ebcf8088e3186d5d2bf8498ee7ed44/rpds_py-0.28.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9699fa7990368b22032baf2b2dce1f634388e4ffc03dfefaaac79f4695edc95", size = 525319 },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/cb/c6734774789566d46775f193964b76627cd5f42ecf246d257ce84d1912ed/rpds_py-0.28.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9b06fe1a75e05e0713f06ea0c89ecb6452210fd60e2f1b6ddc1067b990e08d9", size = 404896 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/53/14e37ce83202c632c89b0691185dca9532288ff9d390eacae3d2ff771bae/rpds_py-0.28.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac9f83e7b326a3f9ec3ef84cda98fb0a74c7159f33e692032233046e7fd15da2", size = 382862 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/83/f3642483ca971a54d60caa4449f9d6d4dbb56a53e0072d0deff51b38af74/rpds_py-0.28.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:0d3259ea9ad8743a75a43eb7819324cdab393263c91be86e2d1901ee65c314e0", size = 398848 },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/09/2d9c8b2f88e399b4cfe86efdf2935feaf0394e4f14ab30c6c5945d60af7d/rpds_py-0.28.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a7548b345f66f6695943b4ef6afe33ccd3f1b638bd9afd0f730dd255c249c9e", size = 412030 },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/f5/e1cec473d4bde6df1fd3738be8e82d64dd0600868e76e92dfeaebbc2d18f/rpds_py-0.28.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9a40040aa388b037eb39416710fbcce9443498d2eaab0b9b45ae988b53f5c67", size = 559700 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/be/73bb241c1649edbf14e98e9e78899c2c5e52bbe47cb64811f44d2cc11808/rpds_py-0.28.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f60c7ea34e78c199acd0d3cda37a99be2c861dd2b8cf67399784f70c9f8e57d", size = 584581 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/9c/ffc6e9218cd1eb5c2c7dbd276c87cd10e8c2232c456b554169eb363381df/rpds_py-0.28.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1571ae4292649100d743b26d5f9c63503bb1fedf538a8f29a98dce2d5ba6b4e6", size = 549981 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/50/da8b6d33803a94df0149345ee33e5d91ed4d25fc6517de6a25587eae4133/rpds_py-0.28.0-cp311-cp311-win32.whl", hash = "sha256:5cfa9af45e7c1140af7321fa0bef25b386ee9faa8928c80dc3a5360971a29e8c", size = 214729 },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/fd/b0f48c4c320ee24c8c20df8b44acffb7353991ddf688af01eef5f93d7018/rpds_py-0.28.0-cp311-cp311-win_amd64.whl", hash = "sha256:dd8d86b5d29d1b74100982424ba53e56033dc47720a6de9ba0259cf81d7cecaa", size = 223977 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/21/c8e77a2ac66e2ec4e21f18a04b4e9a0417ecf8e61b5eaeaa9360a91713b4/rpds_py-0.28.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e27d3a5709cc2b3e013bf93679a849213c79ae0573f9b894b284b55e729e120", size = 217326 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/5c/6c3936495003875fe7b14f90ea812841a08fca50ab26bd840e924097d9c8/rpds_py-0.28.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6b4f28583a4f247ff60cd7bdda83db8c3f5b05a7a82ff20dd4b078571747708f", size = 366439 },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/f9/a0f1ca194c50aa29895b442771f036a25b6c41a35e4f35b1a0ea713bedae/rpds_py-0.28.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d678e91b610c29c4b3d52a2c148b641df2b4676ffe47c59f6388d58b99cdc424", size = 348170 },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/ea/42d243d3a586beb72c77fa5def0487daf827210069a95f36328e869599ea/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e819e0e37a44a78e1383bf1970076e2ccc4dc8c2bbaa2f9bd1dc987e9afff628", size = 378838 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/78/3de32e18a94791af8f33601402d9d4f39613136398658412a4e0b3047327/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5ee514e0f0523db5d3fb171f397c54875dbbd69760a414dccf9d4d7ad628b5bd", size = 393299 },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/7e/4bdb435afb18acea2eb8a25ad56b956f28de7c59f8a1d32827effa0d4514/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3fa06d27fdcee47f07a39e02862da0100cb4982508f5ead53ec533cd5fe55e", size = 518000 },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/d0/5f52a656875cdc60498ab035a7a0ac8f399890cc1ee73ebd567bac4e39ae/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:46959ef2e64f9e4a41fc89aa20dbca2b85531f9a72c21099a3360f35d10b0d5a", size = 408746 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/cd/49ce51767b879cde77e7ad9fae164ea15dce3616fe591d9ea1df51152706/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8455933b4bcd6e83fde3fefc987a023389c4b13f9a58c8d23e4b3f6d13f78c84", size = 386379 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/99/e4e1e1ee93a98f72fc450e36c0e4d99c35370220e815288e3ecd2ec36a2a/rpds_py-0.28.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:ad50614a02c8c2962feebe6012b52f9802deec4263946cddea37aaf28dd25a66", size = 401280 },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/35/e0c6a57488392a8b319d2200d03dad2b29c0db9996f5662c3b02d0b86c02/rpds_py-0.28.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e5deca01b271492553fdb6c7fd974659dce736a15bae5dad7ab8b93555bceb28", size = 412365 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/6a/841337980ea253ec797eb084665436007a1aad0faac1ba097fb906c5f69c/rpds_py-0.28.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:735f8495a13159ce6a0d533f01e8674cec0c57038c920495f87dcb20b3ddb48a", size = 559573 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/5e/64826ec58afd4c489731f8b00729c5f6afdb86f1df1df60bfede55d650bb/rpds_py-0.28.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:961ca621ff10d198bbe6ba4957decca61aa2a0c56695384c1d6b79bf61436df5", size = 583973 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/ee/44d024b4843f8386a4eeaa4c171b3d31d55f7177c415545fd1a24c249b5d/rpds_py-0.28.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2374e16cc9131022e7d9a8f8d65d261d9ba55048c78f3b6e017971a4f5e6353c", size = 553800 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/89/33e675dccff11a06d4d85dbb4d1865f878d5020cbb69b2c1e7b2d3f82562/rpds_py-0.28.0-cp312-cp312-win32.whl", hash = "sha256:d15431e334fba488b081d47f30f091e5d03c18527c325386091f31718952fe08", size = 216954 },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/36/45f6ebb3210887e8ee6dbf1bc710ae8400bb417ce165aaf3024b8360d999/rpds_py-0.28.0-cp312-cp312-win_amd64.whl", hash = "sha256:a410542d61fc54710f750d3764380b53bf09e8c4edbf2f9141a82aa774a04f7c", size = 227844 },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/91/f3fb250d7e73de71080f9a221d19bd6a1c1eb0d12a1ea26513f6c1052ad6/rpds_py-0.28.0-cp312-cp312-win_arm64.whl", hash = "sha256:1f0cfd1c69e2d14f8c892b893997fa9a60d890a0c8a603e88dca4955f26d1edd", size = 217624 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/03/ce566d92611dfac0085c2f4b048cd53ed7c274a5c05974b882a908d540a2/rpds_py-0.28.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e9e184408a0297086f880556b6168fa927d677716f83d3472ea333b42171ee3b", size = 366235 },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/34/1c61da1b25592b86fd285bd7bd8422f4c9d748a7373b46126f9ae792a004/rpds_py-0.28.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:edd267266a9b0448f33dc465a97cfc5d467594b600fe28e7fa2f36450e03053a", size = 348241 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/00/ed1e28616848c61c493a067779633ebf4b569eccaacf9ccbdc0e7cba2b9d/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85beb8b3f45e4e32f6802fb6cd6b17f615ef6c6a52f265371fb916fae02814aa", size = 378079 },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/b2/ccb30333a16a470091b6e50289adb4d3ec656fd9951ba8c5e3aaa0746a67/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d2412be8d00a1b895f8ad827cc2116455196e20ed994bb704bf138fe91a42724", size = 393151 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/d0/73e2217c3ee486d555cb84920597480627d8c0240ff3062005c6cc47773e/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cf128350d384b777da0e68796afdcebc2e9f63f0e9f242217754e647f6d32491", size = 517520 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/91/23efe81c700427d0841a4ae7ea23e305654381831e6029499fe80be8a071/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a2036d09b363aa36695d1cc1a97b36865597f4478470b0697b5ee9403f4fe399", size = 408699 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/ee/a324d3198da151820a326c1f988caaa4f37fc27955148a76fff7a2d787a9/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8e1e9be4fa6305a16be628959188e4fd5cd6f1b0e724d63c6d8b2a8adf74ea6", size = 385720 },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/ad/e68120dc05af8b7cab4a789fccd8cdcf0fe7e6581461038cc5c164cd97d2/rpds_py-0.28.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0a403460c9dd91a7f23fc3188de6d8977f1d9603a351d5db6cf20aaea95b538d", size = 401096 },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/90/c1e070620042459d60df6356b666bb1f62198a89d68881816a7ed121595a/rpds_py-0.28.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d7366b6553cdc805abcc512b849a519167db8f5e5c3472010cd1228b224265cb", size = 411465 },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/61/7c195b30d57f1b8d5970f600efee72a4fad79ec829057972e13a0370fd24/rpds_py-0.28.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b43c6a3726efd50f18d8120ec0551241c38785b68952d240c45ea553912ac41", size = 558832 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/3d/06f3a718864773f69941d4deccdf18e5e47dd298b4628062f004c10f3b34/rpds_py-0.28.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0cb7203c7bc69d7c1585ebb33a2e6074492d2fc21ad28a7b9d40457ac2a51ab7", size = 583230 },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/df/62fc783781a121e77fee9a21ead0a926f1b652280a33f5956a5e7833ed30/rpds_py-0.28.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a52a5169c664dfb495882adc75c304ae1d50df552fbd68e100fdc719dee4ff9", size = 553268 },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/85/d34366e335140a4837902d3dea89b51f087bd6a63c993ebdff59e93ee61d/rpds_py-0.28.0-cp313-cp313-win32.whl", hash = "sha256:2e42456917b6687215b3e606ab46aa6bca040c77af7df9a08a6dcfe8a4d10ca5", size = 217100 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/1c/f25a3f3752ad7601476e3eff395fe075e0f7813fbb9862bd67c82440e880/rpds_py-0.28.0-cp313-cp313-win_amd64.whl", hash = "sha256:e0a0311caedc8069d68fc2bf4c9019b58a2d5ce3cd7cb656c845f1615b577e1e", size = 227759 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/d6/5f39b42b99615b5bc2f36ab90423ea404830bdfee1c706820943e9a645eb/rpds_py-0.28.0-cp313-cp313-win_arm64.whl", hash = "sha256:04c1b207ab8b581108801528d59ad80aa83bb170b35b0ddffb29c20e411acdc1", size = 217326 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/8b/0c69b72d1cee20a63db534be0df271effe715ef6c744fdf1ff23bb2b0b1c/rpds_py-0.28.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f296ea3054e11fc58ad42e850e8b75c62d9a93a9f981ad04b2e5ae7d2186ff9c", size = 355736 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/6d/0c2ee773cfb55c31a8514d2cece856dd299170a49babd50dcffb15ddc749/rpds_py-0.28.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5a7306c19b19005ad98468fcefeb7100b19c79fc23a5f24a12e06d91181193fa", size = 342677 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/1c/22513ab25a27ea205144414724743e305e8153e6abe81833b5e678650f5a/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5d9b86aa501fed9862a443c5c3116f6ead8bc9296185f369277c42542bd646b", size = 371847 },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/07/68e6ccdb4b05115ffe61d31afc94adef1833d3a72f76c9632d4d90d67954/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e5bbc701eff140ba0e872691d573b3d5d30059ea26e5785acba9132d10c8c31d", size = 381800 },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/bf/6d6d15df80781d7f9f368e7c1a00caf764436518c4877fb28b029c4624af/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a5690671cd672a45aa8616d7374fdf334a1b9c04a0cac3c854b1136e92374fe", size = 518827 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/d3/2decbb2976cc452cbf12a2b0aaac5f1b9dc5dd9d1f7e2509a3ee00421249/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9f1d92ecea4fa12f978a367c32a5375a1982834649cdb96539dcdc12e609ab1a", size = 399471 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/2c/f30892f9e54bd02e5faca3f6a26d6933c51055e67d54818af90abed9748e/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d252db6b1a78d0a3928b6190156042d54c93660ce4d98290d7b16b5296fb7cc", size = 377578 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/5d/3bce97e5534157318f29ac06bf2d279dae2674ec12f7cb9c12739cee64d8/rpds_py-0.28.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d61b355c3275acb825f8777d6c4505f42b5007e357af500939d4a35b19177259", size = 390482 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/f0/886bd515ed457b5bd93b166175edb80a0b21a210c10e993392127f1e3931/rpds_py-0.28.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:acbe5e8b1026c0c580d0321c8aae4b0a1e1676861d48d6e8c6586625055b606a", size = 402447 },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/b5/71e8777ac55e6af1f4f1c05b47542a1eaa6c33c1cf0d300dca6a1c6e159a/rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8aa23b6f0fc59b85b4c7d89ba2965af274346f738e8d9fc2455763602e62fd5f", size = 552385 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/cb/6ca2d70cbda5a8e36605e7788c4aa3bea7c17d71d213465a5a675079b98d/rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7b14b0c680286958817c22d76fcbca4800ddacef6f678f3a7c79a1fe7067fe37", size = 575642 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/d4/407ad9960ca7856d7b25c96dcbe019270b5ffdd83a561787bc682c797086/rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bcf1d210dfee61a6c86551d67ee1031899c0fdbae88b2d44a569995d43797712", size = 544507 },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/31/2f46fe0efcac23fbf5797c6b6b7e1c76f7d60773e525cb65fcbc582ee0f2/rpds_py-0.28.0-cp313-cp313t-win32.whl", hash = "sha256:3aa4dc0fdab4a7029ac63959a3ccf4ed605fee048ba67ce89ca3168da34a1342", size = 205376 },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/e4/15947bda33cbedfc134490a41841ab8870a72a867a03d4969d886f6594a2/rpds_py-0.28.0-cp313-cp313t-win_amd64.whl", hash = "sha256:7b7d9d83c942855e4fdcfa75d4f96f6b9e272d42fffcb72cd4bb2577db2e2907", size = 215907 },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/47/ffe8cd7a6a02833b10623bf765fbb57ce977e9a4318ca0e8cf97e9c3d2b3/rpds_py-0.28.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:dcdcb890b3ada98a03f9f2bb108489cdc7580176cb73b4f2d789e9a1dac1d472", size = 353830 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/9f/890f36cbd83a58491d0d91ae0db1702639edb33fb48eeb356f80ecc6b000/rpds_py-0.28.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f274f56a926ba2dc02976ca5b11c32855cbd5925534e57cfe1fda64e04d1add2", size = 341819 },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/e3/921eb109f682aa24fb76207698fbbcf9418738f35a40c21652c29053f23d/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fe0438ac4a29a520ea94c8c7f1754cdd8feb1bc490dfda1bfd990072363d527", size = 373127 },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/13/bce4384d9f8f4989f1a9599c71b7a2d877462e5fd7175e1f69b398f729f4/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8a358a32dd3ae50e933347889b6af9a1bdf207ba5d1a3f34e1a38cd3540e6733", size = 382767 },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/e1/579512b2d89a77c64ccef5a0bc46a6ef7f72ae0cf03d4b26dcd52e57ee0a/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e80848a71c78aa328fefaba9c244d588a342c8e03bda518447b624ea64d1ff56", size = 517585 },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/3c/ca704b8d324a2591b0b0adcfcaadf9c862375b11f2f667ac03c61b4fd0a6/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f586db2e209d54fe177e58e0bc4946bea5fb0102f150b1b2f13de03e1f0976f8", size = 399828 },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/37/e84283b9e897e3adc46b4c88bb3f6ec92a43bd4d2f7ef5b13459963b2e9c/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ae8ee156d6b586e4292491e885d41483136ab994e719a13458055bec14cf370", size = 375509 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/c2/a980beab869d86258bf76ec42dec778ba98151f253a952b02fe36d72b29c/rpds_py-0.28.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a805e9b3973f7e27f7cab63a6b4f61d90f2e5557cff73b6e97cd5b8540276d3d", size = 392014 },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/b5/b1d3c5f9d3fa5aeef74265f9c64de3c34a0d6d5cd3c81c8b17d5c8f10ed4/rpds_py-0.28.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5d3fd16b6dc89c73a4da0b4ac8b12a7ecc75b2864b95c9e5afed8003cb50a728", size = 402410 },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/ae/cab05ff08dfcc052afc73dcb38cbc765ffc86f94e966f3924cd17492293c/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6796079e5d24fdaba6d49bda28e2c47347e89834678f2bc2c1b4fc1489c0fb01", size = 553593 },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/80/50d5706ea2a9bfc9e9c5f401d91879e7c790c619969369800cde202da214/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:76500820c2af232435cbe215e3324c75b950a027134e044423f59f5b9a1ba515", size = 576925 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/12/85a57d7a5855a3b188d024b099fd09c90db55d32a03626d0ed16352413ff/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bbdc5640900a7dbf9dd707fe6388972f5bbd883633eb68b76591044cfe346f7e", size = 542444 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/65/10643fb50179509150eb94d558e8837c57ca8b9adc04bd07b98e57b48f8c/rpds_py-0.28.0-cp314-cp314-win32.whl", hash = "sha256:adc8aa88486857d2b35d75f0640b949759f79dc105f50aa2c27816b2e0dd749f", size = 207968 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/84/0c11fe4d9aaea784ff4652499e365963222481ac647bcd0251c88af646eb/rpds_py-0.28.0-cp314-cp314-win_amd64.whl", hash = "sha256:66e6fa8e075b58946e76a78e69e1a124a21d9a48a5b4766d15ba5b06869d1fa1", size = 218876 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/e0/3ab3b86ded7bb18478392dc3e835f7b754cd446f62f3fc96f4fe2aca78f6/rpds_py-0.28.0-cp314-cp314-win_arm64.whl", hash = "sha256:a6fe887c2c5c59413353b7c0caff25d0e566623501ccfff88957fa438a69377d", size = 212506 },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/ec/d5681bb425226c3501eab50fc30e9d275de20c131869322c8a1729c7b61c/rpds_py-0.28.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7a69df082db13c7070f7b8b1f155fa9e687f1d6aefb7b0e3f7231653b79a067b", size = 355433 },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/ec/568c5e689e1cfb1ea8b875cffea3649260955f677fdd7ddc6176902d04cd/rpds_py-0.28.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b1cde22f2c30ebb049a9e74c5374994157b9b70a16147d332f89c99c5960737a", size = 342601 },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/fe/51ada84d1d2a1d9d8f2c902cfddd0133b4a5eb543196ab5161d1c07ed2ad/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5338742f6ba7a51012ea470bd4dc600a8c713c0c72adaa0977a1b1f4327d6592", size = 372039 },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/c1/60144a2f2620abade1a78e0d91b298ac2d9b91bc08864493fa00451ef06e/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e1460ebde1bcf6d496d80b191d854adedcc619f84ff17dc1c6d550f58c9efbba", size = 382407 },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/ed/091a7bbdcf4038a60a461df50bc4c82a7ed6d5d5e27649aab61771c17585/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e3eb248f2feba84c692579257a043a7699e28a77d86c77b032c1d9fbb3f0219c", size = 518172 },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/dd/02cc90c2fd9c2ef8016fd7813bfacd1c3a1325633ec8f244c47b449fc868/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3bbba5def70b16cd1c1d7255666aad3b290fbf8d0fe7f9f91abafb73611a91", size = 399020 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/81/5d98cc0329bbb911ccecd0b9e19fbf7f3a5de8094b4cda5e71013b2dd77e/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3114f4db69ac5a1f32e7e4d1cbbe7c8f9cf8217f78e6e002cedf2d54c2a548ed", size = 377451 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/07/4d5bcd49e3dfed2d38e2dcb49ab6615f2ceb9f89f5a372c46dbdebb4e028/rpds_py-0.28.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4b0cb8a906b1a0196b863d460c0222fb8ad0f34041568da5620f9799b83ccf0b", size = 390355 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/79/9f14ba9010fee74e4f40bf578735cfcbb91d2e642ffd1abe429bb0b96364/rpds_py-0.28.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf681ac76a60b667106141e11a92a3330890257e6f559ca995fbb5265160b56e", size = 403146 },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/4c/f08283a82ac141331a83a40652830edd3a4a92c34e07e2bbe00baaea2f5f/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1e8ee6413cfc677ce8898d9cde18cc3a60fc2ba756b0dec5b71eb6eb21c49fa1", size = 552656 },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/47/d922fc0666f0dd8e40c33990d055f4cc6ecff6f502c2d01569dbed830f9b/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b3072b16904d0b5572a15eb9d31c1954e0d3227a585fc1351aa9878729099d6c", size = 576782 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/0c/5bafdd8ccf6aa9d3bfc630cfece457ff5b581af24f46a9f3590f790e3df2/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b670c30fd87a6aec281c3c9896d3bae4b205fd75d79d06dc87c2503717e46092", size = 544671 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/37/dcc5d8397caa924988693519069d0beea077a866128719351a4ad95e82fc/rpds_py-0.28.0-cp314-cp314t-win32.whl", hash = "sha256:8014045a15b4d2b3476f0a287fcc93d4f823472d7d1308d47884ecac9e612be3", size = 205749 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/69/64d43b21a10d72b45939a28961216baeb721cc2a430f5f7c3bfa21659a53/rpds_py-0.28.0-cp314-cp314t-win_amd64.whl", hash = "sha256:7a4e59c90d9c27c561eb3160323634a9ff50b04e4f7820600a2beb0ac90db578", size = 216233 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/bc/b43f2ea505f28119bd551ae75f70be0c803d2dbcd37c1b3734909e40620b/rpds_py-0.28.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f5e7101145427087e493b9c9b959da68d357c28c562792300dd21a095118ed16", size = 363913 },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/f2/db318195d324c89a2c57dc5195058cbadd71b20d220685c5bd1da79ee7fe/rpds_py-0.28.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:31eb671150b9c62409a888850aaa8e6533635704fe2b78335f9aaf7ff81eec4d", size = 350452 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/f2/1391c819b8573a4898cedd6b6c5ec5bc370ce59e5d6bdcebe3c9c1db4588/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48b55c1f64482f7d8bd39942f376bfdf2f6aec637ee8c805b5041e14eeb771db", size = 380957 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/5c/e5de68ee7eb7248fce93269833d1b329a196d736aefb1a7481d1e99d1222/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24743a7b372e9a76171f6b69c01aedf927e8ac3e16c474d9fe20d552a8cb45c7", size = 391919 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/4f/2376336112cbfeb122fd435d608ad8d5041b3aed176f85a3cb32c262eb80/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:389c29045ee8bbb1627ea190b4976a310a295559eaf9f1464a1a6f2bf84dde78", size = 528541 },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/53/5ae232e795853dd20da7225c5dd13a09c0a905b1a655e92bdf8d78a99fd9/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:23690b5827e643150cf7b49569679ec13fe9a610a15949ed48b85eb7f98f34ec", size = 405629 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/2d/351a3b852b683ca9b6b8b38ed9efb2347596973849ba6c3a0e99877c10aa/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f0c9266c26580e7243ad0d72fc3e01d6b33866cfab5084a6da7576bcf1c4f72", size = 384123 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/15/870804daa00202728cc91cb8e2385fa9f1f4eb49857c49cfce89e304eae6/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:4c6c4db5d73d179746951486df97fd25e92396be07fc29ee8ff9a8f5afbdfb27", size = 400923 },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/25/3706b83c125fa2a0bccceac951de3f76631f6bd0ee4d02a0ed780712ef1b/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3b695a8fa799dd2cfdb4804b37096c5f6dba1ac7f48a7fbf6d0485bcd060316", size = 413767 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/f9/ce43dbe62767432273ed2584cef71fef8411bddfb64125d4c19128015018/rpds_py-0.28.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:6aa1bfce3f83baf00d9c5fcdbba93a3ab79958b4c7d7d1f55e7fe68c20e63912", size = 561530 },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/c9/ffe77999ed8f81e30713dd38fd9ecaa161f28ec48bb80fa1cd9118399c27/rpds_py-0.28.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:7b0f9dceb221792b3ee6acb5438eb1f02b0cb2c247796a72b016dcc92c6de829", size = 585453 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/d2/4a73b18821fd4669762c855fd1f4e80ceb66fb72d71162d14da58444a763/rpds_py-0.28.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:5d0145edba8abd3db0ab22b5300c99dc152f5c9021fab861be0f0544dc3cbc5f", size = 552199 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "safetensors"
|
||||
version = "0.6.2"
|
||||
|
|
@ -1996,6 +2506,15 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smmap"
|
||||
version = "5.0.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
|
|
@ -2005,6 +2524,15 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "soupsieve"
|
||||
version = "2.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6d/e6/21ccce3262dd4889aa3332e5a119a3491a95e8f60939870a3a035aabac0d/soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f", size = 103472 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlalchemy"
|
||||
version = "2.0.44"
|
||||
|
|
@ -2055,6 +2583,35 @@ 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 = "streamlit"
|
||||
version = "1.51.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "altair" },
|
||||
{ name = "blinker" },
|
||||
{ name = "cachetools" },
|
||||
{ name = "click" },
|
||||
{ name = "gitpython" },
|
||||
{ name = "numpy" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pandas" },
|
||||
{ name = "pillow" },
|
||||
{ name = "protobuf" },
|
||||
{ name = "pyarrow" },
|
||||
{ name = "pydeck" },
|
||||
{ name = "requests" },
|
||||
{ name = "tenacity" },
|
||||
{ name = "toml" },
|
||||
{ name = "tornado" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "watchdog", marker = "sys_platform != 'darwin'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/59/6d/327ddd5fc35fcf2aeecb4040668337f5565a1c6c95b1e892b8bfd4bb9031/streamlit-1.51.0.tar.gz", hash = "sha256:1e742a9c0b698f466c6f5bf58d333beda5a1fbe8de660743976791b5c1446ef6", size = 9742904 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/39/60/868371b6482ccd9ef423c6f62650066cf8271fdb2ee84f192695ad6b7a96/streamlit-1.51.0-py3-none-any.whl", hash = "sha256:4008b029f71401ce54946bb09a6a3e36f4f7652cbb48db701224557738cfda38", size = 10171702 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sympy"
|
||||
version = "1.14.0"
|
||||
|
|
@ -2164,6 +2721,15 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/b3/46/e33a8c93907b631a99377ef4c5f817ab453d0b34f93529421f42ff559671/tokenizers-0.22.1-cp39-abi3-win_amd64.whl", hash = "sha256:65fd6e3fb11ca1e78a6a93602490f134d1fdeb13bcef99389d5102ea318ed138", size = 2674684 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.10.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "torch"
|
||||
version = "2.9.0"
|
||||
|
|
@ -2220,6 +2786,25 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/fc/29/bd361e0cbb2c79ce6450f42643aaf6919956f89923a50571b0ebfe92d142/torch-2.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:695ba920f234ad4170c9c50e28d56c848432f8f530e6bc7f88fcb15ddf338e75", size = 109503850 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tornado"
|
||||
version = "6.5.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/09/ce/1eb500eae19f4648281bb2186927bb062d2438c2e5093d1360391afd2f90/tornado-6.5.2.tar.gz", hash = "sha256:ab53c8f9a0fa351e2c0741284e06c7a45da86afb544133201c5cc8578eb076a0", size = 510821 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/48/6a7529df2c9cc12efd2e8f5dd219516184d703b34c06786809670df5b3bd/tornado-6.5.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2436822940d37cde62771cff8774f4f00b3c8024fe482e16ca8387b8a2724db6", size = 442563 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/b5/9b575a0ed3e50b00c40b08cbce82eb618229091d09f6d14bce80fc01cb0b/tornado-6.5.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:583a52c7aa94ee046854ba81d9ebb6c81ec0fd30386d96f7640c96dad45a03ef", size = 440729 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/4e/619174f52b120efcf23633c817fd3fed867c30bff785e2cd5a53a70e483c/tornado-6.5.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0fe179f28d597deab2842b86ed4060deec7388f1fd9c1b4a41adf8af058907e", size = 444295 },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/fa/87b41709552bbd393c85dd18e4e3499dcd8983f66e7972926db8d96aa065/tornado-6.5.2-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b186e85d1e3536d69583d2298423744740986018e393d0321df7340e71898882", size = 443644 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/41/fb15f06e33d7430ca89420283a8762a4e6b8025b800ea51796ab5e6d9559/tornado-6.5.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e792706668c87709709c18b353da1f7662317b563ff69f00bab83595940c7108", size = 443878 },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/92/fe6d57da897776ad2e01e279170ea8ae726755b045fe5ac73b75357a5a3f/tornado-6.5.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:06ceb1300fd70cb20e43b1ad8aaee0266e69e7ced38fa910ad2e03285009ce7c", size = 444549 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/02/c8f4f6c9204526daf3d760f4aa555a7a33ad0e60843eac025ccfd6ff4a93/tornado-6.5.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:74db443e0f5251be86cbf37929f84d8c20c27a355dd452a5cfa2aada0d001ec4", size = 443973 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/2d/f5f5707b655ce2317190183868cd0f6822a1121b4baeae509ceb9590d0bd/tornado-6.5.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b5e735ab2889d7ed33b32a459cac490eda71a1ba6857b0118de476ab6c366c04", size = 443954 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/59/593bd0f40f7355806bf6573b47b8c22f8e1374c9b6fd03114bd6b7a3dcfd/tornado-6.5.2-cp39-abi3-win32.whl", hash = "sha256:c6f29e94d9b37a95013bb669616352ddb82e3bfe8326fccee50583caebc8a5f0", size = 445023 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/2a/f609b420c2f564a748a2d80ebfb2ee02a73ca80223af712fca591386cafb/tornado-6.5.2-cp39-abi3-win_amd64.whl", hash = "sha256:e56a5af51cc30dd2cae649429af65ca2f6571da29504a07995175df14c18f35f", size = 445427 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/4f/e1f65e8f8c76d73658b33d33b81eed4322fb5085350e4328d5c956f0c8f9/tornado-6.5.2-cp39-abi3-win_arm64.whl", hash = "sha256:d6c33dc3672e3a1f3618eb63b7ef4683a7688e7b9e6e8f0d9aa5726360a004af", size = 444456 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tqdm"
|
||||
version = "4.67.1"
|
||||
|
|
@ -2394,6 +2979,24 @@ wheels = [
|
|||
{ 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 = "watchdog"
|
||||
version = "6.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078 },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078 },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065 },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070 },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "watchfiles"
|
||||
version = "1.1.1"
|
||||
|
|
|
|||
Loading…
Reference in a new issue