perf think improvements
This commit is contained in:
parent
45b3a68332
commit
ba048ff96c
15 changed files with 48908 additions and 32742 deletions
|
|
@ -13,10 +13,14 @@ benchmarks/
|
|||
│ ├── locomo_benchmark.py # LoComo-specific implementations
|
||||
│ ├── run_benchmark.py # Runner script
|
||||
│ └── locomo10.json # Dataset (place here)
|
||||
└── longmemeval/ # LongMemEval benchmark
|
||||
├── longmemeval_benchmark.py # LongMemEval-specific implementations
|
||||
├── run_benchmark.py # Runner script
|
||||
└── longmemeval_s_cleaned.json # Dataset (auto-downloaded)
|
||||
├── longmemeval/ # LongMemEval benchmark
|
||||
│ ├── longmemeval_benchmark.py # LongMemEval-specific implementations
|
||||
│ ├── run_benchmark.py # Runner script
|
||||
│ └── longmemeval_s_cleaned.json # Dataset (auto-downloaded)
|
||||
└── visualizer/ # Web-based benchmark visualizer
|
||||
├── server.py # FastAPI server
|
||||
├── serve.sh # Launch script
|
||||
└── static/ # Frontend assets (HTML, CSS, JS)
|
||||
```
|
||||
|
||||
## Common Framework
|
||||
|
|
@ -42,15 +46,23 @@ The common framework provides a unified interface with optimizations from the wo
|
|||
uv run python run_benchmark.py
|
||||
```
|
||||
|
||||
2. **Run quick test** (1 conversation, 10 questions):
|
||||
2. **Run with think API** (integrated search + answer generation, skips separate search step):
|
||||
```bash
|
||||
cd locomo
|
||||
uv run python run_benchmark.py --use-think
|
||||
```
|
||||
|
||||
Note: Think mode uses the memory system's integrated `think_async()` API which performs its own retrieval and reasoning in a single call, making it more efficient than the traditional two-step approach.
|
||||
|
||||
3. **Run quick test** (1 conversation, 10 questions):
|
||||
```bash
|
||||
cd locomo
|
||||
uv run python run_benchmark.py --max-conversations 1 --max-questions 10
|
||||
```
|
||||
|
||||
3. **View results**:
|
||||
- Detailed report: `locomo/RESULTS.md`
|
||||
- Raw data: `locomo/benchmark_results.json`
|
||||
4. **View results**:
|
||||
- Detailed report: `locomo/RESULTS.md` or `locomo/results_table_think.md`
|
||||
- Raw data: `locomo/benchmark_results.json` or `locomo/benchmark_results_think.json`
|
||||
|
||||
### Dataset
|
||||
|
||||
|
|
@ -150,6 +162,40 @@ Based on published results:
|
|||
- **Estimated runtime**: 2-4 hours
|
||||
- **Estimated cost**: $50-80 (OpenAI API)
|
||||
|
||||
## Benchmark Visualizer
|
||||
|
||||
**Location**: `visualizer/`
|
||||
|
||||
**Purpose**: Web-based interface for visualizing and analyzing benchmark results.
|
||||
|
||||
### Quick Start
|
||||
|
||||
1. **Start the visualizer**:
|
||||
```bash
|
||||
cd visualizer
|
||||
./serve.sh
|
||||
```
|
||||
|
||||
2. **Open browser**: http://localhost:8001
|
||||
|
||||
3. **Select benchmark**: Choose from:
|
||||
- "LoComo (search)" - Traditional search + LLM
|
||||
- "LoComo (think)" - Integrated think API
|
||||
|
||||
### Features
|
||||
|
||||
- Interactive visualization of benchmark results
|
||||
- Category-wise performance breakdown (Multi-hop, Single-hop, Temporal, Open-domain)
|
||||
- Filter by correctness (all/correct/incorrect answers)
|
||||
- Detailed Q&A view with reasoning and retrieved memories
|
||||
- Overall and per-item accuracy statistics
|
||||
- Think mode displays fact types with color-coded borders:
|
||||
- Green: World facts
|
||||
- Orange: Agent facts
|
||||
- Purple: Opinion facts
|
||||
|
||||
See `visualizer/README.md` for more details.
|
||||
|
||||
## Future Benchmarks
|
||||
|
||||
- **MemGPT Tasks**: Long-context question answering
|
||||
|
|
|
|||
|
|
@ -8,6 +8,13 @@ optimizations as the working LoComo benchmark:
|
|||
- Parallel LLM judging with rate limiting
|
||||
- Progress tracking with Rich
|
||||
- Comprehensive metrics collection
|
||||
- Support for both traditional (search + LLM) and integrated (think API) approaches
|
||||
|
||||
The framework supports two answer generation patterns:
|
||||
1. Traditional: Benchmark runner performs search, then passes results to answer generator
|
||||
2. Integrated: Answer generator performs its own retrieval (e.g., think API)
|
||||
- Indicated by needs_external_search() returning False
|
||||
- Skips the search step for efficiency
|
||||
"""
|
||||
|
||||
import json
|
||||
|
|
@ -70,17 +77,32 @@ class BenchmarkDataset(ABC):
|
|||
class LLMAnswerGenerator(ABC):
|
||||
"""Abstract base class for LLM-based answer generation."""
|
||||
|
||||
def needs_external_search(self) -> bool:
|
||||
"""
|
||||
Whether this generator needs external search to be performed.
|
||||
|
||||
Returns:
|
||||
True if the benchmark runner should perform search before calling generate_answer.
|
||||
False if the generator does its own retrieval (e.g., integrated think API).
|
||||
"""
|
||||
return True
|
||||
|
||||
@abstractmethod
|
||||
async def generate_answer(
|
||||
self,
|
||||
question: str,
|
||||
memories: List[Dict[str, Any]]
|
||||
) -> Tuple[str, str]:
|
||||
) -> Tuple[str, str, Optional[List[Dict[str, Any]]]]:
|
||||
"""
|
||||
Generate answer from retrieved memories.
|
||||
|
||||
Returns:
|
||||
Tuple of (answer, reasoning)
|
||||
Tuple of (answer, reasoning, retrieved_memories_override)
|
||||
- answer: The generated answer text
|
||||
- reasoning: Explanation of how the answer was derived
|
||||
- retrieved_memories_override: Optional list of memories to include in results
|
||||
- None: Use memories passed in (traditional mode)
|
||||
- List: Use these memories instead (integrated mode like think API)
|
||||
"""
|
||||
pass
|
||||
|
||||
|
|
@ -183,25 +205,39 @@ class BenchmarkRunner:
|
|||
Returns:
|
||||
Tuple of (answer, reasoning, retrieved_memories)
|
||||
"""
|
||||
# Search memory
|
||||
results, _ = await self.memory.search_async(
|
||||
agent_id=agent_id,
|
||||
query=question,
|
||||
thinking_budget=thinking_budget,
|
||||
top_k=top_k,
|
||||
weight_activation=weight_activation,
|
||||
weight_semantic=weight_semantic,
|
||||
weight_recency=weight_recency,
|
||||
weight_frequency=weight_frequency,
|
||||
)
|
||||
# Check if generator needs external search
|
||||
if self.answer_generator.needs_external_search():
|
||||
# Traditional flow: search then generate
|
||||
results, _ = await self.memory.search_async(
|
||||
agent_id=agent_id,
|
||||
query=question,
|
||||
thinking_budget=thinking_budget,
|
||||
top_k=top_k,
|
||||
weight_activation=weight_activation,
|
||||
weight_semantic=weight_semantic,
|
||||
weight_recency=weight_recency,
|
||||
weight_frequency=weight_frequency,
|
||||
)
|
||||
|
||||
if not results:
|
||||
return "I don't have enough information to answer that question.", "No relevant memories found.", []
|
||||
if not results:
|
||||
return "I don't have enough information to answer that question.", "No relevant memories found.", []
|
||||
|
||||
# Generate answer using LLM
|
||||
answer, reasoning = await self.answer_generator.generate_answer(question, results)
|
||||
# Generate answer using LLM
|
||||
answer, reasoning, memories_override = await self.answer_generator.generate_answer(question, results)
|
||||
|
||||
return answer, reasoning, results
|
||||
# Use override if provided, otherwise use search results
|
||||
final_memories = memories_override if memories_override is not None else results
|
||||
|
||||
return answer, reasoning, final_memories
|
||||
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, [])
|
||||
|
||||
# Use memories from generator (should not be None for integrated mode)
|
||||
final_memories = memories_override if memories_override is not None else []
|
||||
|
||||
return answer, reasoning, final_memories
|
||||
|
||||
async def evaluate_qa_task(
|
||||
self,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -131,12 +131,13 @@ class LoComoAnswerGenerator(LLMAnswerGenerator):
|
|||
self,
|
||||
question: str,
|
||||
memories: List[Dict[str, Any]]
|
||||
) -> Tuple[str, str]:
|
||||
) -> Tuple[str, str, Optional[List[Dict[str, Any]]]]:
|
||||
"""
|
||||
Generate answer from retrieved memories using Groq.
|
||||
|
||||
Returns:
|
||||
Tuple of (answer, reasoning)
|
||||
Tuple of (answer, reasoning, None)
|
||||
- None indicates to use the memories passed in
|
||||
"""
|
||||
# Format context
|
||||
context_parts = []
|
||||
|
|
@ -203,9 +204,122 @@ Answer:
|
|||
response_format=QuestionAnswer
|
||||
)
|
||||
answer_obj = response.choices[0].message.parsed
|
||||
return answer_obj.answer, answer_obj.reasoning
|
||||
return answer_obj.answer, answer_obj.reasoning, None
|
||||
except Exception as e:
|
||||
return f"Error generating answer: {str(e)}", "Error occurred during answer generation."
|
||||
return f"Error generating answer: {str(e)}", "Error occurred during answer generation.", None
|
||||
|
||||
|
||||
class LoComoThinkAnswerGenerator(LLMAnswerGenerator):
|
||||
"""LoComo answer generator using the think API instead of search + LLM.
|
||||
|
||||
This generator performs its own retrieval internally via the think API,
|
||||
so it doesn't need external search to be performed by the benchmark runner.
|
||||
"""
|
||||
|
||||
def __init__(self, memory: 'TemporalSemanticMemory', agent_id: str, thinking_budget: int = 500, top_k: int = 20):
|
||||
"""Initialize with memory instance and agent_id.
|
||||
|
||||
Args:
|
||||
memory: TemporalSemanticMemory instance
|
||||
agent_id: Agent identifier for think queries
|
||||
thinking_budget: Budget for memory exploration
|
||||
top_k: Maximum number of facts to retrieve
|
||||
"""
|
||||
self.memory = memory
|
||||
self.agent_id = agent_id
|
||||
self.thinking_budget = thinking_budget
|
||||
self.top_k = top_k
|
||||
|
||||
def needs_external_search(self) -> bool:
|
||||
"""Think API does its own retrieval, so no external search needed."""
|
||||
return False
|
||||
|
||||
async def generate_answer(
|
||||
self,
|
||||
question: str,
|
||||
memories: List[Dict[str, Any]]
|
||||
) -> Tuple[str, str, Optional[List[Dict[str, Any]]]]:
|
||||
"""
|
||||
Generate answer using the integrated think API.
|
||||
|
||||
The think API performs both search and answer generation in a single call,
|
||||
combining agent facts, world facts, and opinions to formulate a response.
|
||||
|
||||
Args:
|
||||
question: Question to answer
|
||||
memories: Not used (empty list), as think does its own retrieval
|
||||
|
||||
Returns:
|
||||
Tuple of (answer, reasoning, retrieved_memories)
|
||||
- retrieved_memories: Combined list of all facts from based_on (world, agent, opinion)
|
||||
"""
|
||||
try:
|
||||
# Use the think API which does both search and answer generation
|
||||
result = await self.memory.think_async(
|
||||
agent_id=self.agent_id,
|
||||
query=question,
|
||||
thinking_budget=self.thinking_budget,
|
||||
top_k=self.top_k,
|
||||
model="openai/gpt-oss-120b",
|
||||
temperature=0.7,
|
||||
max_tokens=1000
|
||||
)
|
||||
|
||||
# Extract answer and reasoning
|
||||
answer = result.get('text', '')
|
||||
|
||||
# Extract memories from based_on
|
||||
based_on = result.get('based_on', {})
|
||||
world_facts = based_on.get('world', [])
|
||||
agent_facts = based_on.get('agent', [])
|
||||
opinion_facts = based_on.get('opinion', [])
|
||||
|
||||
# Combine all facts into retrieved_memories
|
||||
retrieved_memories = []
|
||||
|
||||
# Add world facts
|
||||
for fact in world_facts:
|
||||
retrieved_memories.append({
|
||||
'id': fact.get('id'),
|
||||
'text': fact.get('text'),
|
||||
'context': fact.get('context'),
|
||||
'event_date': fact.get('event_date'),
|
||||
'score': fact.get('score', 0.0),
|
||||
'fact_type': 'world'
|
||||
})
|
||||
|
||||
# Add agent facts
|
||||
for fact in agent_facts:
|
||||
retrieved_memories.append({
|
||||
'id': fact.get('id'),
|
||||
'text': fact.get('text'),
|
||||
'context': fact.get('context'),
|
||||
'event_date': fact.get('event_date'),
|
||||
'score': fact.get('score', 0.0),
|
||||
'fact_type': 'agent'
|
||||
})
|
||||
|
||||
# Add opinion facts
|
||||
for fact in opinion_facts:
|
||||
retrieved_memories.append({
|
||||
'id': fact.get('id'),
|
||||
'text': fact.get('text'),
|
||||
'context': fact.get('context'),
|
||||
'event_date': fact.get('event_date'),
|
||||
'score': fact.get('score', 0.0),
|
||||
'fact_type': 'opinion'
|
||||
})
|
||||
|
||||
# Build reasoning summary
|
||||
num_world = len(world_facts)
|
||||
num_agent = len(agent_facts)
|
||||
num_opinion = len(opinion_facts)
|
||||
|
||||
reasoning = f"Think API: {num_world} world facts, {num_agent} agent facts, {num_opinion} opinions"
|
||||
|
||||
return answer, reasoning, retrieved_memories
|
||||
except Exception as e:
|
||||
return f"Error generating answer: {str(e)}", "Error occurred during think API call.", []
|
||||
|
||||
|
||||
class JudgeResponse(pydantic.BaseModel):
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
# LoComo Benchmark Results
|
||||
|
||||
**Overall Accuracy**: 62.00% (62/100)
|
||||
**Overall Accuracy**: 65.33% (98/150)
|
||||
|
||||
| Sample ID | Sessions | Questions | Correct | Accuracy | Multi-hop | Single-hop | Temporal | Open-domain |
|
||||
|-----------|----------|-----------|---------|----------|-----------|------------|----------|-------------|
|
||||
| conv-26 | 19 | 50 | 30 | 60.00% | N/A | N/A | N/A | N/A |
|
||||
| conv-30 | 19 | 50 | 32 | 64.00% | N/A | N/A | N/A | N/A |
|
||||
| conv-26 | 19 | 150 | 98 | 65.33% | N/A | N/A | N/A | N/A |
|
||||
|
|
@ -15,14 +15,15 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
|
|||
import asyncio
|
||||
import argparse
|
||||
from memora import TemporalSemanticMemory
|
||||
from locomo_benchmark import LoComoDataset, LoComoAnswerGenerator, LoComoAnswerEvaluator
|
||||
from locomo_benchmark import LoComoDataset, LoComoAnswerGenerator, LoComoThinkAnswerGenerator, LoComoAnswerEvaluator
|
||||
from common.benchmark_runner import BenchmarkRunner
|
||||
|
||||
|
||||
async def run_benchmark(
|
||||
max_conversations: int = None,
|
||||
max_questions_per_conv: int = None,
|
||||
skip_ingestion: bool = False
|
||||
skip_ingestion: bool = False,
|
||||
use_think: bool = False
|
||||
):
|
||||
"""
|
||||
Run the LoComo benchmark.
|
||||
|
|
@ -31,14 +32,26 @@ async def run_benchmark(
|
|||
max_conversations: Maximum number of conversations to evaluate (None for all)
|
||||
max_questions_per_conv: Maximum questions per conversation (None for all)
|
||||
skip_ingestion: Whether to skip ingestion and use existing data
|
||||
use_think: Whether to use the think API instead of search + LLM
|
||||
"""
|
||||
# Initialize components
|
||||
dataset = LoComoDataset()
|
||||
answer_generator = LoComoAnswerGenerator()
|
||||
answer_evaluator = LoComoAnswerEvaluator()
|
||||
memory = TemporalSemanticMemory()
|
||||
await memory.initialize()
|
||||
|
||||
# Select answer generator based on mode
|
||||
if use_think:
|
||||
answer_generator = LoComoThinkAnswerGenerator(
|
||||
memory=memory,
|
||||
agent_id="locomo",
|
||||
thinking_budget=500,
|
||||
top_k=20
|
||||
)
|
||||
else:
|
||||
answer_generator = LoComoAnswerGenerator()
|
||||
|
||||
answer_evaluator = LoComoAnswerEvaluator()
|
||||
|
||||
# Create benchmark runner
|
||||
runner = BenchmarkRunner(
|
||||
dataset=dataset,
|
||||
|
|
@ -63,15 +76,19 @@ async def run_benchmark(
|
|||
|
||||
# Display and save results
|
||||
runner.display_results(results)
|
||||
runner.save_results(results, Path(__file__).parent / 'benchmark_results.json')
|
||||
|
||||
# Determine output filename based on mode
|
||||
suffix = "_think" if use_think else ""
|
||||
results_filename = f'benchmark_results{suffix}.json'
|
||||
runner.save_results(results, Path(__file__).parent / results_filename)
|
||||
|
||||
# Generate markdown table
|
||||
generate_markdown_table(results)
|
||||
generate_markdown_table(results, use_think)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def generate_markdown_table(results: dict):
|
||||
def generate_markdown_table(results: dict, use_think: bool = False):
|
||||
"""
|
||||
Generate a markdown table with benchmark results.
|
||||
|
||||
|
|
@ -93,7 +110,8 @@ def generate_markdown_table(results: dict):
|
|||
|
||||
# Build markdown content
|
||||
lines = []
|
||||
lines.append("# LoComo Benchmark Results")
|
||||
mode_str = " (Think Mode)" if use_think else ""
|
||||
lines.append(f"# LoComo Benchmark Results{mode_str}")
|
||||
lines.append("")
|
||||
lines.append(f"**Overall Accuracy**: {results['overall_accuracy']:.2f}% ({results['total_correct']}/{results['total_questions']})")
|
||||
lines.append("")
|
||||
|
|
@ -123,8 +141,9 @@ def generate_markdown_table(results: dict):
|
|||
f"{cat_accuracies['3']} | {cat_accuracies['4']} |"
|
||||
)
|
||||
|
||||
# Write to file
|
||||
output_file = Path(__file__).parent / 'results_table.md'
|
||||
# Write to file with suffix
|
||||
suffix = "_think" if use_think else ""
|
||||
output_file = Path(__file__).parent / f'results_table{suffix}.md'
|
||||
output_file.write_text('\n'.join(lines))
|
||||
console.print(f"\n[green]✓[/green] Results table saved to {output_file}")
|
||||
|
||||
|
|
@ -137,11 +156,13 @@ if __name__ == "__main__":
|
|||
parser.add_argument('--max-conversations', type=int, default=None, help='Maximum conversations to evaluate')
|
||||
parser.add_argument('--max-questions', type=int, default=None, help='Maximum questions per conversation')
|
||||
parser.add_argument('--skip-ingestion', action='store_true', help='Skip ingestion and use existing data')
|
||||
parser.add_argument('--use-think', action='store_true', help='Use think API instead of search + LLM')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
results = asyncio.run(run_benchmark(
|
||||
max_conversations=args.max_conversations,
|
||||
max_questions_per_conv=args.max_questions,
|
||||
skip_ingestion=args.skip_ingestion
|
||||
skip_ingestion=args.skip_ingestion,
|
||||
use_think=args.use_think
|
||||
))
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ class Entity(BaseModel):
|
|||
text: str = Field(
|
||||
description="The entity name as it appears in the fact"
|
||||
)
|
||||
type: Literal["PERSON", "ORG", "PLACE", "PRODUCT", "CONCEPT"] = Field(
|
||||
description="Entity type: PERSON, ORG, PLACE, PRODUCT, or CONCEPT"
|
||||
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"
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -167,16 +167,22 @@ Each fact should:
|
|||
## TEMPORAL INFORMATION (VERY IMPORTANT)
|
||||
For each fact, extract the ABSOLUTE date/time when it occurred:
|
||||
- If text mentions ABSOLUTE dates ("on March 15, 2024", "last Tuesday"), use that date
|
||||
- If text mentions RELATIVE times ("yesterday", "last week", "this morning", "3 days ago"), calculate the absolute date using the reference date above.
|
||||
- if text mentions a vague relative time without a specific day ("last week", "this morning"), transform the date in relative with absolute context ("last week" + " 2 june 2024" -> "week before June 2 2024") in the text and use the absolute date for the 'date' field
|
||||
- If text mentions RELATIVE times ("yesterday", "last week", "last month", "last year", "this morning", "3 days ago", "next year"), calculate the absolute date using the reference date above
|
||||
- **CRITICAL**: Transform relative temporal expressions in the FACT TEXT to absolute context:
|
||||
- "last year" → "in [calculated year]" (e.g., if reference is 2023, "last year" becomes "in 2022")
|
||||
- "last month" → "in [month name] [year]" (e.g., if reference is March 2024, "last month" becomes "in February 2024")
|
||||
- "last week" → "week of [date]" or keep as "last week" with absolute date field
|
||||
- "yesterday" → can stay as "yesterday" with absolute date field
|
||||
- If NO specific time is mentioned, use the reference date
|
||||
- Always output dates in ISO format: YYYY-MM-DDTHH:MM:SSZ
|
||||
|
||||
Examples of date extraction:
|
||||
Examples of date extraction and fact text transformation:
|
||||
- Reference: 2024-03-20T10:00:00Z
|
||||
- "Yesterday I went hiking" → date: 2024-03-19T10:00:00Z
|
||||
- "Last week I joined Google" → date: 2024-03-13T10:00:00Z (approximately)
|
||||
- "This morning I had coffee" → date: 2024-03-20T08:00:00Z
|
||||
- "Yesterday I went hiking" → fact: "Yesterday I went hiking", date: 2024-03-19T10:00:00Z
|
||||
- "Last week I joined Google" → fact: "Last week I joined Google", date: 2024-03-13T10:00:00Z (approximately)
|
||||
- "Last year we visited Paris" → fact: "In 2023 we visited Paris", date: 2023-03-20T10:00:00Z
|
||||
- "Last month I started a new job" → fact: "In February 2024 I started a new job", date: 2024-02-20T10:00:00Z
|
||||
- "This morning I had coffee" → fact: "This morning I had coffee", date: 2024-03-20T08:00:00Z
|
||||
- "I work at Google" (no time mentioned) → date: 2024-03-20T10:00:00Z (use reference)
|
||||
|
||||
## What to EXTRACT (BE EXHAUSTIVE - DO NOT SKIP ANYTHING):
|
||||
|
|
@ -237,10 +243,12 @@ For EACH fact, extract ALL important entities mentioned with their types:
|
|||
- **PLACE**: Cities, countries, locations, venues (Mountain View, Yosemite, The Coffee Shop)
|
||||
- **PRODUCT**: Specific products, tools, technologies (iPhone, Python, TensorFlow)
|
||||
- **CONCEPT**: Important topics, projects, subjects (AI, machine learning, Project Phoenix)
|
||||
- **OTHER**: Entities that don't fit the above categories (events, time periods, etc.)
|
||||
|
||||
Entity extraction rules:
|
||||
- Use the EXACT form as it appears in the fact (preserve capitalization)
|
||||
- Assign the correct type to distinguish ambiguous entities (Apple the company = ORG, apple the fruit = PRODUCT/CONCEPT)
|
||||
- Use OTHER only for entities that truly don't fit the other categories
|
||||
- Include both full names and commonly used short forms if both appear
|
||||
- Extract proper nouns and key identifying terms
|
||||
- Skip generic terms (the, a, some) and pronouns (he, she, they)
|
||||
|
|
@ -249,15 +257,16 @@ Entity extraction rules:
|
|||
## EXAMPLES of GOOD facts (detailed, comprehensive):
|
||||
|
||||
Input: "Alice mentioned she works at Google in Mountain View. She joined the AI team last year."
|
||||
GOOD fact: "Alice works at Google in Mountain View on the AI team, which she joined last year"
|
||||
GOOD fact: "Alice works at Google in Mountain View on the AI team, which she joined in 2023"
|
||||
GOOD fact_type: "world"
|
||||
GOOD date: Calculate based on reference date (if reference is 2024-03-20, "last year" = 2023-03-20)
|
||||
GOOD date: 2023-03-20T10:00:00Z (if reference is 2024-03-20, "last year" = 2023)
|
||||
GOOD entities: [
|
||||
{{"text": "Alice", "type": "PERSON"}},
|
||||
{{"text": "Google", "type": "ORG"}},
|
||||
{{"text": "Mountain View", "type": "PLACE"}},
|
||||
{{"text": "AI team", "type": "ORG"}}
|
||||
]
|
||||
NOTE: "last year" was transformed to "in 2023" in the fact text
|
||||
|
||||
Input: "Yesterday Bob went hiking in Yosemite because it helps him clear his mind."
|
||||
GOOD fact: "Bob went hiking in Yosemite because it helps him clear his mind"
|
||||
|
|
@ -299,6 +308,15 @@ GOOD entities: [
|
|||
]
|
||||
NOTE: Use type to distinguish "Apple" the company from "apples" the fruit
|
||||
|
||||
Input: "The conference starts on Monday at the convention center."
|
||||
GOOD fact: "The conference starts on Monday at the convention center"
|
||||
GOOD entities: [
|
||||
{{"text": "conference", "type": "OTHER"}},
|
||||
{{"text": "Monday", "type": "OTHER"}},
|
||||
{{"text": "convention center", "type": "PLACE"}}
|
||||
]
|
||||
NOTE: Use OTHER for entities like events (conference) or time references (Monday) that don't fit other categories
|
||||
|
||||
Input: "Melanie said 'Yesterday I took the kids to the museum - it was so cool seeing their eyes light up!'"
|
||||
BAD fact: "The kids were excited about the museum"
|
||||
BAD entities: [{{"text": "museum", "type": "PLACE"}}]
|
||||
|
|
@ -338,44 +356,65 @@ Remember:
|
|||
6. Include ALL details, names, numbers, reasons, and context in the fact text
|
||||
7. Extract the absolute date for EACH fact by calculating relative times from the reference date
|
||||
8. **CLASSIFY EACH FACT**: 'world' for general facts, 'agent' for AI agent actions
|
||||
9. Extract ALL entities with their types (PERSON, ORG, PLACE, PRODUCT, CONCEPT) for each fact
|
||||
9. Extract ALL entities with their types (PERSON, ORG, PLACE, PRODUCT, CONCEPT, OTHER) for each fact
|
||||
10. Use types to disambiguate entities (Apple the company = ORG, apple the fruit = PRODUCT)
|
||||
11. When in doubt, EXTRACT IT - better to have too many facts than miss important events"""
|
||||
11. Use OTHER for entities that don't fit other categories (events, time periods, etc.)
|
||||
12. When in doubt, EXTRACT IT - better to have too many facts than miss important events"""
|
||||
|
||||
import time
|
||||
import logging
|
||||
from openai import BadRequestError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
llm_call_start = time.time()
|
||||
response = await client.beta.chat.completions.parse(
|
||||
model=model,
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are an EXHAUSTIVE fact and entity extractor. CRITICAL RULES: 1) ALWAYS include the SUBJECT (never 'the kids' without whose kids), 2) Extract biographical details as SEPARATE facts (if someone mentions 'my home country Sweden', extract 'Person is from Sweden' as its own fact), 3) Extract EVERY event, action, and fact - never skip anything. For each fact, extract ALL important entities with their types: PERSON, ORG, PLACE, PRODUCT, CONCEPT. Use types to disambiguate (Apple=ORG vs apples=PRODUCT). Preserve possessive relationships (their→whose). Include casual mentions (photos, meetups). Calculate absolute dates from relative times. When in doubt, extract it - better too many facts than missing critical biographical/identity information."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": prompt
|
||||
}
|
||||
],
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
response_format=FactExtractionResponse,
|
||||
extra_body={"service_tier": "auto"},
|
||||
)
|
||||
llm_call_time = time.time() - llm_call_start
|
||||
# Retry logic for JSON validation errors
|
||||
max_retries = 2
|
||||
last_error = None
|
||||
|
||||
# Extract the parsed response
|
||||
extraction_response = response.choices[0].message.parsed
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
llm_call_start = time.time()
|
||||
response = await client.beta.chat.completions.parse(
|
||||
model=model,
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are an EXHAUSTIVE fact and entity extractor. CRITICAL RULES: 1) ALWAYS include the SUBJECT (never 'the kids' without whose kids), 2) Extract biographical details as SEPARATE facts (if someone mentions 'my home country Sweden', extract 'Person is from Sweden' as its own fact), 3) Extract EVERY event, action, and fact - never skip anything. 4) **TRANSFORM RELATIVE DATES IN FACT TEXT**: Convert 'last year' to 'in [year]', 'last month' to 'in [month year]' using the reference date - DO NOT leave relative temporal expressions like 'last year' or 'last month' in the fact text. For each fact, extract ALL important entities with their types: PERSON, ORG, PLACE, PRODUCT, CONCEPT, OTHER (for entities that don't fit other categories). Use types to disambiguate (Apple=ORG vs apples=PRODUCT). Preserve possessive relationships (their→whose). Include casual mentions (photos, meetups). Calculate absolute dates from relative times. When in doubt, extract it - better too many facts than missing critical biographical/identity information."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": prompt
|
||||
}
|
||||
],
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
response_format=FactExtractionResponse,
|
||||
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]
|
||||
# Extract the parsed response
|
||||
extraction_response = response.choices[0].message.parsed
|
||||
|
||||
logger.info(f" [1.3.{chunk_index + 1}] Chunk {chunk_index + 1}/{total_chunks} LLM call: {len(chunk_facts)} facts from {len(chunk)} chars in {llm_call_time:.3f}s")
|
||||
# Convert to dict format
|
||||
chunk_facts = [fact.model_dump() for fact in extraction_response.facts]
|
||||
|
||||
return chunk_facts
|
||||
logger.info(f" [1.3.{chunk_index + 1}] Chunk {chunk_index + 1}/{total_chunks} LLM call: {len(chunk_facts)} facts from {len(chunk)} chars in {llm_call_time:.3f}s")
|
||||
|
||||
return chunk_facts
|
||||
|
||||
except BadRequestError as e:
|
||||
last_error = e
|
||||
if "json_validate_failed" in str(e):
|
||||
logger.warning(f" [1.3.{chunk_index + 1}] Attempt {attempt + 1}/{max_retries} failed with JSON validation error: {e}")
|
||||
if attempt < max_retries - 1:
|
||||
logger.info(f" [1.3.{chunk_index + 1}] Retrying...")
|
||||
continue
|
||||
# If it's not a JSON validation error or we're out of retries, re-raise
|
||||
raise
|
||||
|
||||
# If we exhausted all retries, raise the last error
|
||||
raise last_error
|
||||
|
||||
|
||||
async def extract_facts_from_text(
|
||||
|
|
|
|||
|
|
@ -7,13 +7,14 @@ This directory contains specialized operation modules for the TemporalSemanticMe
|
|||
✅ **Successfully Completed!**
|
||||
|
||||
**File Size Reduction:**
|
||||
- Before: 1,720 lines (temporal_semantic_memory.py)
|
||||
- After: 1,420 lines (temporal_semantic_memory.py)
|
||||
- **Removed: 300 lines (17% reduction)**
|
||||
- Before: 2,065 lines (temporal_semantic_memory.py)
|
||||
- After: 1,846 lines (temporal_semantic_memory.py)
|
||||
- **Removed: 219 lines (11% reduction)**
|
||||
|
||||
**Modules Created:**
|
||||
- `embedding_operations.py` - Embedding generation with process pool parallelism
|
||||
- `link_operations.py` - Entity, temporal, and semantic link creation (300+ lines)
|
||||
- `think_operations.py` - Think operations with opinion handling (230+ lines)
|
||||
- `batch_operations.py` - Placeholder for future extraction
|
||||
- `search_operations.py` - Placeholder for future extraction
|
||||
|
||||
|
|
@ -25,6 +26,7 @@ The memory system now uses a **mixin pattern** for better code organization:
|
|||
class TemporalSemanticMemory(
|
||||
EmbeddingOperationsMixin,
|
||||
LinkOperationsMixin,
|
||||
ThinkOperationsMixin,
|
||||
):
|
||||
"""
|
||||
Advanced memory system using temporal and semantic linking.
|
||||
|
|
@ -32,6 +34,7 @@ class TemporalSemanticMemory(
|
|||
Mixins provide:
|
||||
- EmbeddingOperationsMixin: _generate_embedding, _generate_embeddings_batch
|
||||
- LinkOperationsMixin: Entity, temporal, semantic link operations
|
||||
- ThinkOperationsMixin: think_async, _extract_opinions_from_text
|
||||
"""
|
||||
# Core infrastructure and batch operations
|
||||
pass
|
||||
|
|
@ -50,13 +53,19 @@ class TemporalSemanticMemory(
|
|||
- `_create_semantic_links_batch()` - Meaning-based connections
|
||||
- `_insert_entity_links_batch()` - Batch link insertion
|
||||
|
||||
### ThinkOperationsMixin (think_operations.py)
|
||||
- `think_async()` - Formulate answers using agent, world, and opinion facts
|
||||
- `_extract_opinions_from_text()` - Extract opinions from generated text with LLM
|
||||
- Parallel fact retrieval with `asyncio.gather`
|
||||
- Opinion formation and storage as background tasks
|
||||
|
||||
### Remaining in Main Class
|
||||
- Database connection management (`__init__`, `_get_pool`, `close`)
|
||||
- Batch storage operations (`put`, `put_async`, `put_batch_async`)
|
||||
- Search operations (`search`, `search_async`, `_apply_mmr`)
|
||||
- Document management (`get_document`, `delete_document`, `delete_agent`)
|
||||
- Think operations (`think_async`)
|
||||
- Deduplication (`_find_duplicate_facts_batch`)
|
||||
- Opinion evaluation (`_evaluate_opinion_update_async`)
|
||||
|
||||
## Benefits Achieved
|
||||
|
||||
|
|
|
|||
|
|
@ -6,8 +6,10 @@ This package contains specialized operation modules for the TemporalSemanticMemo
|
|||
|
||||
from .embedding_operations import EmbeddingOperationsMixin
|
||||
from .link_operations import LinkOperationsMixin
|
||||
from .think_operations import ThinkOperationsMixin
|
||||
|
||||
__all__ = [
|
||||
'EmbeddingOperationsMixin',
|
||||
'LinkOperationsMixin',
|
||||
'ThinkOperationsMixin',
|
||||
]
|
||||
|
|
|
|||
|
|
@ -2,9 +2,13 @@
|
|||
Think operations for formulating answers based on agent and world facts.
|
||||
"""
|
||||
|
||||
import os
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from typing import Dict, List, Any
|
||||
from openai import AsyncOpenAI
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ThinkOperationsMixin:
|
||||
|
|
@ -16,69 +20,112 @@ class ThinkOperationsMixin:
|
|||
query: str,
|
||||
thinking_budget: int = 50,
|
||||
top_k: int = 10,
|
||||
model: str = "llama-3.3-70b-versatile",
|
||||
model: str = "openai/gpt-oss-120b",
|
||||
temperature: float = 0.7,
|
||||
max_tokens: int = 1000,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Think and formulate an answer using agent identity and world facts.
|
||||
Think and formulate an answer using agent identity, world facts, and opinions.
|
||||
|
||||
This method:
|
||||
1. Retrieves agent facts (agent's identity and past actions)
|
||||
2. Retrieves world facts (general knowledge)
|
||||
3. Uses Groq LLM to formulate an answer
|
||||
4. Returns plain text answer and the facts used
|
||||
3. Retrieves existing opinions (agent's formed perspectives)
|
||||
4. Uses Groq LLM to formulate an answer
|
||||
5. Extracts and stores any new opinions formed during thinking
|
||||
6. Returns plain text answer and the facts used
|
||||
|
||||
Args:
|
||||
agent_id: Agent identifier
|
||||
query: Question to answer
|
||||
thinking_budget: Number of memory units to explore
|
||||
top_k: Maximum facts to retrieve
|
||||
model: LLM model to use (default: llama-3.3-70b-versatile)
|
||||
model: LLM model to use (default: openai/gpt-oss-120b)
|
||||
temperature: Sampling temperature
|
||||
max_tokens: Maximum tokens in response
|
||||
|
||||
Returns:
|
||||
Dict with:
|
||||
- text: Plain text answer (no markdown)
|
||||
- based_on: Dict with 'world' and 'agent' fact lists
|
||||
- based_on: Dict with 'world', 'agent', and 'opinion' fact lists
|
||||
- new_opinions: List of newly formed opinions
|
||||
"""
|
||||
# Initialize Groq client
|
||||
groq_api_key = os.getenv("GROQ_API_KEY")
|
||||
if not groq_api_key:
|
||||
# Use cached LLM client
|
||||
if self._llm_client is None:
|
||||
raise ValueError("GROQ_API_KEY environment variable not set")
|
||||
|
||||
client = AsyncOpenAI(
|
||||
api_key=groq_api_key,
|
||||
base_url="https://api.groq.com/openai/v1"
|
||||
client = self._llm_client
|
||||
|
||||
# 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,
|
||||
top_k=top_k,
|
||||
enable_trace=False,
|
||||
fact_type='agent'
|
||||
),
|
||||
# Get world facts
|
||||
self.search_async(
|
||||
agent_id=agent_id,
|
||||
query=query,
|
||||
thinking_budget=thinking_budget,
|
||||
top_k=top_k,
|
||||
enable_trace=False,
|
||||
fact_type='world'
|
||||
),
|
||||
# Get existing opinions
|
||||
self.search_async(
|
||||
agent_id=agent_id,
|
||||
query=query,
|
||||
thinking_budget=thinking_budget,
|
||||
top_k=top_k,
|
||||
enable_trace=False,
|
||||
fact_type='opinion'
|
||||
)
|
||||
)
|
||||
|
||||
# Step 1: Get agent facts (identity)
|
||||
agent_results, _ = await self.search_async(
|
||||
agent_id=agent_id,
|
||||
query=query,
|
||||
thinking_budget=thinking_budget,
|
||||
top_k=top_k,
|
||||
enable_trace=False,
|
||||
fact_type='agent'
|
||||
)
|
||||
# Step 4: Format facts for LLM with full details as JSON
|
||||
import json
|
||||
|
||||
# Step 2: Get world facts
|
||||
world_results, _ = await self.search_async(
|
||||
agent_id=agent_id,
|
||||
query=query,
|
||||
thinking_budget=thinking_budget,
|
||||
top_k=top_k,
|
||||
enable_trace=False,
|
||||
fact_type='world'
|
||||
)
|
||||
def format_facts(facts):
|
||||
if not facts:
|
||||
return "[]"
|
||||
formatted = []
|
||||
for fact in facts:
|
||||
fact_obj = {
|
||||
"text": fact['text']
|
||||
}
|
||||
|
||||
# Step 3: Format facts for LLM
|
||||
agent_facts_text = "\n".join([f"- {fact['text']}" for fact in agent_results]) if agent_results else "None"
|
||||
world_facts_text = "\n".join([f"- {fact['text']}" for fact in world_results]) if world_results else "None"
|
||||
# Add context if available
|
||||
if fact.get('context'):
|
||||
fact_obj["context"] = fact['context']
|
||||
|
||||
# Step 4: Call Groq to formulate answer
|
||||
prompt = f"""You are an AI assistant answering a question based on retrieved facts.
|
||||
# Add event_date if available
|
||||
if fact.get('event_date'):
|
||||
from datetime import datetime
|
||||
event_date = fact['event_date']
|
||||
if isinstance(event_date, str):
|
||||
fact_obj["event_date"] = event_date
|
||||
elif isinstance(event_date, datetime):
|
||||
fact_obj["event_date"] = event_date.strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
# Add score if available
|
||||
if fact.get('score') is not None:
|
||||
fact_obj["score"] = fact['score']
|
||||
|
||||
formatted.append(fact_obj)
|
||||
|
||||
return json.dumps(formatted, indent=2)
|
||||
|
||||
agent_facts_text = format_facts(agent_results)
|
||||
world_facts_text = format_facts(world_results)
|
||||
opinion_facts_text = format_facts(opinion_results)
|
||||
|
||||
# Step 5: Call Groq to formulate answer
|
||||
prompt = f"""You are an AI assistant answering a question based on retrieved facts provided in JSON format.
|
||||
|
||||
AGENT IDENTITY (what the agent has done):
|
||||
{agent_facts_text}
|
||||
|
|
@ -86,14 +133,25 @@ AGENT IDENTITY (what the agent has done):
|
|||
WORLD FACTS (general knowledge):
|
||||
{world_facts_text}
|
||||
|
||||
YOUR EXISTING OPINIONS (perspectives you've formed):
|
||||
{opinion_facts_text}
|
||||
|
||||
QUESTION: {query}
|
||||
|
||||
Provide a helpful, accurate answer based on the facts above. If the facts don't contain enough information to answer the question, say so clearly. Do not use markdown formatting - respond in plain text only."""
|
||||
The facts above are provided as JSON arrays. Each fact may include:
|
||||
- text: The fact content
|
||||
- context: Additional context information
|
||||
- event_date: When the fact occurred
|
||||
- score: Relevance score
|
||||
|
||||
Provide a helpful, accurate answer based on the facts above. Be consistent with your existing opinions. If the facts don't contain enough information to answer the question, say so clearly. Do not use markdown formatting - respond in plain text only.
|
||||
|
||||
If you form any new opinions while thinking about this question, state them clearly in your answer."""
|
||||
|
||||
response = await client.chat.completions.create(
|
||||
model=model,
|
||||
messages=[
|
||||
{"role": "system", "content": "You are a helpful AI assistant. Always respond in plain text without markdown formatting."},
|
||||
{"role": "system", "content": "You are a helpful AI assistant. Always respond in plain text without markdown formatting. You can form and express opinions based on facts."},
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
temperature=temperature,
|
||||
|
|
@ -102,11 +160,109 @@ Provide a helpful, accurate answer based on the facts above. If the facts don't
|
|||
|
||||
answer_text = response.choices[0].message.content.strip()
|
||||
|
||||
# Step 5: Return response with facts split by type
|
||||
# Step 6: Extract new opinions from the answer
|
||||
new_opinions = await self._extract_opinions_from_text(
|
||||
client=client,
|
||||
text=answer_text,
|
||||
model=model
|
||||
)
|
||||
|
||||
# Step 7: Store new opinions (schedule as background tasks, don't wait)
|
||||
if new_opinions:
|
||||
current_time = datetime.now(timezone.utc)
|
||||
for opinion_dict in new_opinions:
|
||||
task = asyncio.create_task(
|
||||
self.put_async(
|
||||
agent_id=agent_id,
|
||||
content=opinion_dict["text"],
|
||||
context=f"formed during thinking about: {query}",
|
||||
event_date=current_time,
|
||||
fact_type_override='opinion',
|
||||
confidence_score=opinion_dict["confidence"]
|
||||
)
|
||||
)
|
||||
# Track task and auto-remove when done
|
||||
self._background_tasks.add(task)
|
||||
task.add_done_callback(self._background_tasks.discard)
|
||||
|
||||
# Step 8: Return response with facts split by type
|
||||
return {
|
||||
"text": answer_text,
|
||||
"based_on": {
|
||||
"world": world_results,
|
||||
"agent": agent_results
|
||||
}
|
||||
"agent": agent_results,
|
||||
"opinion": opinion_results
|
||||
},
|
||||
"new_opinions": new_opinions
|
||||
}
|
||||
|
||||
async def _extract_opinions_from_text(
|
||||
self,
|
||||
client,
|
||||
text: str,
|
||||
model: str
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Extract opinions with reasons and confidence from text using LLM.
|
||||
|
||||
Args:
|
||||
client: OpenAI client
|
||||
text: Text to extract opinions from
|
||||
model: LLM model to use
|
||||
|
||||
Returns:
|
||||
List of dicts with keys: 'text' (opinion with reasons), 'confidence' (score 0-1)
|
||||
"""
|
||||
class Opinion(BaseModel):
|
||||
"""An opinion formed by the agent."""
|
||||
opinion: str = Field(description="The opinion or perspective formed")
|
||||
reasons: str = Field(description="The reasons supporting this opinion")
|
||||
confidence: float = Field(description="Confidence score for this opinion (0.0 to 1.0, where 1.0 is very confident)")
|
||||
|
||||
class OpinionExtractionResponse(BaseModel):
|
||||
"""Response containing extracted opinions."""
|
||||
opinions: List[Opinion] = Field(
|
||||
default_factory=list,
|
||||
description="List of opinions formed with their supporting reasons and confidence scores"
|
||||
)
|
||||
|
||||
extraction_prompt = f"""Extract any opinions or perspectives that were formed in the following text.
|
||||
An opinion is a judgment, viewpoint, or conclusion that goes beyond just stating facts.
|
||||
|
||||
TEXT:
|
||||
{text}
|
||||
|
||||
For each opinion found, provide:
|
||||
1. The opinion itself
|
||||
2. The reasons or facts that support it
|
||||
3. A confidence score (0.0 to 1.0) indicating how confident the agent is in this opinion based on the available information
|
||||
|
||||
If no clear opinions are expressed, return an empty list."""
|
||||
|
||||
try:
|
||||
response = await client.beta.chat.completions.parse(
|
||||
model=model,
|
||||
messages=[
|
||||
{"role": "system", "content": "You extract opinions and perspectives from text."},
|
||||
{"role": "user", "content": extraction_prompt}
|
||||
],
|
||||
response_format=OpinionExtractionResponse
|
||||
)
|
||||
|
||||
result = response.choices[0].message.parsed
|
||||
|
||||
# Format opinions with reasons included in the text and confidence score
|
||||
formatted_opinions = []
|
||||
for op in result.opinions:
|
||||
# Combine opinion and reasons into a single statement
|
||||
opinion_with_reasons = f"{op.opinion} (Reasons: {op.reasons})"
|
||||
formatted_opinions.append({
|
||||
"text": opinion_with_reasons,
|
||||
"confidence": op.confidence
|
||||
})
|
||||
|
||||
return formatted_opinions
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to extract opinions: {str(e)}")
|
||||
return []
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -585,22 +585,6 @@ def _register_routes(app: FastAPI):
|
|||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.get("/api/locomo")
|
||||
async def api_locomo():
|
||||
"""Get Locomo benchmark results."""
|
||||
import json
|
||||
try:
|
||||
results_path = Path(__file__).parent.parent / "benchmarks" / "locomo" / "benchmark_results.json"
|
||||
if not results_path.exists():
|
||||
raise HTTPException(status_code=404, detail="Benchmark results not found")
|
||||
|
||||
with open(results_path, 'r') as f:
|
||||
data = json.load(f)
|
||||
return data
|
||||
except FileNotFoundError:
|
||||
raise HTTPException(status_code=404, detail="Benchmark results not found")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# Create default app instance
|
||||
|
|
|
|||
|
|
@ -1,274 +0,0 @@
|
|||
// Locomo benchmark tab functionality
|
||||
|
||||
let locomoData = null;
|
||||
|
||||
window.loadLocomoResults = async function() {
|
||||
try {
|
||||
const response = await fetch('api/locomo');
|
||||
locomoData = await response.json();
|
||||
console.log('Loaded locomo data:', locomoData);
|
||||
renderLocomoResults();
|
||||
} catch (e) {
|
||||
console.error('Error loading benchmark results:', e);
|
||||
document.getElementById('locomo-content').innerHTML = `
|
||||
<div class="error-message">Error loading benchmark results: ${e.message}<br>
|
||||
Check console for details.</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderLocomoResults() {
|
||||
if (!locomoData) return;
|
||||
|
||||
const content = document.getElementById('locomo-content');
|
||||
|
||||
try {
|
||||
// Handle both old and new structure
|
||||
const results = locomoData.item_results || locomoData.conversation_results || [];
|
||||
const numItems = locomoData.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 }, // category 1
|
||||
2: { name: 'Single-hop', correct: 0, total: 0 }, // category 2
|
||||
3: { name: 'Temporal', correct: 0, total: 0 }, // category 3
|
||||
4: { name: 'Open-domain', correct: 0, total: 0 } // category 4
|
||||
};
|
||||
|
||||
// Aggregate across all items
|
||||
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_correct) {
|
||||
categoryStats[category].correct++;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Overall stats
|
||||
const overallHtml = `
|
||||
<div style="background: #f9f9f9; padding: 20px; border: 2px solid #333; border-radius: 8px; margin-bottom: 20px;">
|
||||
<h3 style="margin-top: 0;">Overall Performance</h3>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">Overall Accuracy</div>
|
||||
<div class="stat-value">${locomoData.overall_accuracy.toFixed(2)}%</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">Correct Answers</div>
|
||||
<div class="stat-value">${locomoData.total_correct} / ${locomoData.total_questions}</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">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 accuracy = cat.total > 0 ? ((cat.correct / cat.total) * 100).toFixed(1) : 0;
|
||||
const color = accuracy >= 70 ? '#43a047' : accuracy >= 50 ? '#ff9800' : '#e53935';
|
||||
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}</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>
|
||||
</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 isCorrect = result.is_correct;
|
||||
const bgColor = isCorrect ? '#e8f5e9' : '#ffebee';
|
||||
const icon = isCorrect ? '✅' : '❌';
|
||||
const category = getCategoryName(result.category);
|
||||
|
||||
html += `
|
||||
<div class="qa-item" data-correct="${isCorrect}" style="background: ${bgColor}; padding: 15px; margin-bottom: 15px; border: 1px solid #ddd; border-radius: 8px;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 10px;">
|
||||
<div style="flex: 1;">
|
||||
<div style="font-weight: bold; font-size: 16px; margin-bottom: 8px;">
|
||||
${icon} Question ${idx + 1} <span style="font-size: 12px; background: #666; color: white; padding: 2px 8px; border-radius: 4px; margin-left: 8px;">${category}</span>
|
||||
</div>
|
||||
<div style="margin-bottom: 8px;">
|
||||
<b>Q:</b> ${result.question}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 10px;">
|
||||
<div>
|
||||
<div style="font-weight: bold; color: #43a047; margin-bottom: 4px;">✓ Correct Answer:</div>
|
||||
<div style="background: white; padding: 8px; border-radius: 4px; border: 1px solid #ccc;">
|
||||
${result.correct_answer}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-weight: bold; color: ${isCorrect ? '#43a047' : '#e53935'}; margin-bottom: 4px;">
|
||||
${isCorrect ? '✓' : '✗'} Predicted Answer:
|
||||
</div>
|
||||
<div style="background: white; padding: 8px; border-radius: 4px; border: 1px solid #ccc;">
|
||||
${result.predicted_answer}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<details style="margin-top: 10px;">
|
||||
<summary style="cursor: pointer; font-weight: bold; padding: 5px; background: rgba(255,255,255,0.5); border-radius: 4px;">
|
||||
📝 Show Reasoning & Retrieved Memories
|
||||
</summary>
|
||||
<div style="margin-top: 10px; padding: 10px; background: white; border-radius: 4px;">
|
||||
<div style="margin-bottom: 10px;">
|
||||
<b>System Reasoning:</b>
|
||||
<div style="padding: 8px; background: #f5f5f5; border-radius: 4px; margin-top: 4px;">
|
||||
${result.reasoning}
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-bottom: 10px;">
|
||||
<b>Judge Reasoning:</b>
|
||||
<div style="padding: 8px; background: #f5f5f5; border-radius: 4px; margin-top: 4px;">
|
||||
${result.correctness_reasoning || 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<b>Retrieved Memories (${result.retrieved_memories ? result.retrieved_memories.length : 0}):</b>
|
||||
${renderRetrievedMemories(result.retrieved_memories)}
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderRetrievedMemories(memories) {
|
||||
if (!memories || !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;
|
||||
html += `
|
||||
<div style="padding: 8px; background: #f5f5f5; border-left: 3px solid #42a5f5; margin-bottom: 8px;">
|
||||
<div style="font-size: 11px; color: #666; margin-bottom: 4px;">
|
||||
Rank #${idx + 1} | Score: ${mem.score ? mem.score.toFixed(4) : 'N/A'}
|
||||
</div>
|
||||
<div style="font-size: 13px;">${mem.text}</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function getCategoryName(category) {
|
||||
const categories = {
|
||||
1: 'Multi-hop',
|
||||
2: 'Single-hop',
|
||||
3: 'Temporal',
|
||||
4: 'Open-domain'
|
||||
};
|
||||
return categories[category] || 'Unknown';
|
||||
}
|
||||
|
||||
function toggleConversation(idx) {
|
||||
const elem = document.getElementById(`conv-${idx}`);
|
||||
if (elem.style.display === 'none') {
|
||||
elem.style.display = 'block';
|
||||
} else {
|
||||
elem.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function filterAnswers() {
|
||||
const filter = document.querySelector('input[name="answer-filter"]:checked').value;
|
||||
const items = document.querySelectorAll('.qa-item');
|
||||
|
||||
items.forEach(item => {
|
||||
const isCorrect = item.dataset.correct === 'true';
|
||||
|
||||
if (filter === 'all') {
|
||||
item.style.display = 'block';
|
||||
} else if (filter === 'correct' && isCorrect) {
|
||||
item.style.display = 'block';
|
||||
} else if (filter === 'incorrect' && !isCorrect) {
|
||||
item.style.display = 'block';
|
||||
} else {
|
||||
item.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -22,7 +22,6 @@
|
|||
<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>
|
||||
<button class="tab-button" onclick="switchMainTab('benchmark')">Benchmark</button>
|
||||
</div>
|
||||
|
||||
<!-- Data Tab -->
|
||||
|
|
@ -284,23 +283,8 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div id="benchmark-tab" class="tab-content">
|
||||
<h2>Benchmark</h2>
|
||||
<p style="color: #666; margin-bottom: 15px; padding: 0 20px;">
|
||||
Run and analyze benchmark results.
|
||||
</p>
|
||||
<div style="margin-bottom: 15px; padding: 0 20px;">
|
||||
<button onclick="loadLocomoResults()" class="load-button">
|
||||
📊 Load Locomo Benchmark
|
||||
</button>
|
||||
</div>
|
||||
<div id="locomo-content" style="padding: 20px;">
|
||||
<p style="text-align: center; color: #666;">Click "Load Locomo Benchmark" to view results</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="./static/js/app.js"></script>
|
||||
<script src="./static/js/locomo.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -63,9 +63,8 @@ async def test_think_opinion_consistency():
|
|||
new_opinions_count = len(result1.get('new_opinions', []))
|
||||
print(f"\nNew opinions formed: {new_opinions_count}")
|
||||
|
||||
# Wait a moment to ensure opinions are stored and any background tasks complete
|
||||
import asyncio
|
||||
await asyncio.sleep(2.0)
|
||||
# Wait for background opinion PUT tasks to complete
|
||||
await memory.wait_for_background_tasks()
|
||||
|
||||
# Search for stored opinions to verify they were actually saved
|
||||
pool = await memory._get_pool()
|
||||
|
|
|
|||
Loading…
Reference in a new issue