Files
Aperant/apps/backend/context/builder.py
T
Andy 0adaddacaa Feature/apps restructure v2.7.2 (#138)
* refactor: restructure project to Apps/frontend and Apps/backend

- Move auto-claude-ui to Apps/frontend with feature-based architecture
- Move auto-claude to Apps/backend
- Switch from pnpm to npm for frontend
- Update Node.js requirement to v24.12.0 LTS
- Add pre-commit hooks for lint, typecheck, and security audit
- Add commit-msg hook for conventional commits
- Fix CommonJS compatibility issues (postcss.config, postinstall scripts)
- Update README with comprehensive setup and contribution guidelines
- Configure ESLint to ignore .cjs files
- 0 npm vulnerabilities

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* feat(refactor): clean code and move to npm

* feat(refactor): clean code and move to npm

* chore: update to v2.7.0, remove Docker deps (LadybugDB is embedded)

* feat: v2.8.0 - update workflows and configs for Apps/ structure, npm

* fix: resolve Python lint errors (F401, I001)

* fix: update test paths for Apps/backend structure

* fix: add missing facade files and update paths for Apps/backend structure

- Fix ruff lint error I001 in auto_claude_tools.py
- Create missing facade files to match upstream (agent, ci_discovery, critique, etc.)
- Update test paths from auto-claude/ to Apps/backend/
- Update .pre-commit-config.yaml paths for Apps/ structure
- Add pytest to pre-commit hooks (skip slow/integration/Windows-incompatible tests)
- Fix Unicode encoding in test_agent_architecture.py for Windows

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* feat: improve readme

* fix: new path

* fix: correct release workflow and docs for Apps/ restructure

- Fix ARM64 macOS build: pnpm → npm, auto-claude-ui → Apps/frontend
- Fix artifact upload paths in release.yml
- Update Node.js version to 24 for consistency
- Update CLI-USAGE.md with Apps/backend paths
- Update RELEASE.md with Apps/frontend/package.json paths

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: rename Apps/ to apps/ and fix backend path resolution

- Rename Apps/ folder to apps/ for consistency with JS/Node conventions
- Update all path references across CI/CD workflows, docs, and config files
- Fix frontend Python path resolver to look for 'backend' instead of 'auto-claude'
- Update path-resolver.ts to correctly find apps/backend in development mode

This completes the Apps restructure from PR #122 and prepares for v2.8.0 release.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(electron): correct preload script path from .js to .mjs

electron-vite builds the preload script as ESM (index.mjs) but the main
process was looking for CommonJS (index.js). This caused the preload to
fail silently, making the app fall back to browser mock mode with fake
data and non-functional IPC handlers.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* - Introduced `dev:debug` script to enable debugging during development.
- Added `dev:mcp` script for running the frontend in MCP mode.

These enhancements streamline the development process for frontend developers.

* refactor(memory): make Graphiti memory mandatory and remove Docker dependency

Memory is now a core component of Auto Claude rather than optional:
- Python 3.12+ is required for the backend (not just memory layer)
- Graphiti is enabled by default in .env.example
- Removed all FalkorDB/Docker references (migrated to embedded LadybugDB)
- Deleted guides/DOCKER-SETUP.md and docker-handlers.ts
- Updated onboarding UI to remove "optional" language
- Updated all documentation to reflect LadybugDB architecture

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add cross-platform Windows support for npm scripts

- Add scripts/install-backend.js for cross-platform Python venv setup
  - Auto-detects Python 3.12 (py -3.12 on Windows, python3.12 on Unix)
  - Handles platform-specific venv paths
- Add scripts/test-backend.js for cross-platform pytest execution
- Update package.json to use Node.js scripts instead of shell commands
- Update CONTRIBUTING.md with correct paths and instructions:
  - apps/backend/ and apps/frontend/ paths
  - Python 3.12 requirement (memory system now required)
  - Platform-specific install commands (winget, brew, apt)
  - npm instead of pnpm
  - Quick Start section with npm run install:all

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* remove doc

* fix(frontend): correct Ollama detector script path after apps restructure

The Ollama status check was failing because memory-handlers.ts
was looking for ollama_model_detector.py at auto-claude/ but the
script is now at apps/backend/ after the directory restructure.

This caused "Ollama not running" to display even when Ollama was
actually running and accessible.

* chore: bump version to 2.7.2

Downgrade version from 2.8.0 to 2.7.2 as the Apps/ restructure
is better suited as a patch release rather than a minor release.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: update package-lock.json for Windows compatibility

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs(contributing): add hotfix workflow and update paths for apps/ structure

Add Git Flow hotfix workflow documentation with step-by-step guide
and ASCII diagram showing the branching strategy.

Update all paths from auto-claude/auto-claude-ui to apps/backend/apps/frontend
and migrate package manager references from pnpm to npm to match the
new project structure.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(ci): remove duplicate ARM64 build from Intel runner

The Intel runner was building both x64 and arm64 architectures,
while a separate ARM64 runner also builds arm64 natively. This
caused duplicate ARM64 builds, wasting CI resources.

Now each runner builds only its native architecture:
- Intel runner: x64 only
- ARM64 runner: arm64 only

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Alex Madera <e.a_madera@hotmail.com>
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-22 21:34:51 +01:00

245 lines
8.5 KiB
Python

"""
Context Builder
===============
Main builder class that orchestrates context building for tasks.
"""
import asyncio
import json
from dataclasses import asdict
from pathlib import Path
from .categorizer import FileCategorizer
from .graphiti_integration import fetch_graph_hints, is_graphiti_enabled
from .keyword_extractor import KeywordExtractor
from .models import FileMatch, TaskContext
from .pattern_discovery import PatternDiscoverer
from .search import CodeSearcher
from .service_matcher import ServiceMatcher
class ContextBuilder:
"""Builds task-specific context by searching the codebase."""
def __init__(self, project_dir: Path, project_index: dict | None = None):
self.project_dir = project_dir.resolve()
self.project_index = project_index or self._load_project_index()
# Initialize components
self.searcher = CodeSearcher(self.project_dir)
self.service_matcher = ServiceMatcher(self.project_index)
self.keyword_extractor = KeywordExtractor()
self.categorizer = FileCategorizer()
self.pattern_discoverer = PatternDiscoverer(self.project_dir)
def _load_project_index(self) -> dict:
"""Load project index from file or create new one (.auto-claude is the installed instance)."""
index_file = self.project_dir / ".auto-claude" / "project_index.json"
if index_file.exists():
with open(index_file) as f:
return json.load(f)
# Try to create one
from analyzer import analyze_project
return analyze_project(self.project_dir)
def build_context(
self,
task: str,
services: list[str] | None = None,
keywords: list[str] | None = None,
include_graph_hints: bool = True,
) -> TaskContext:
"""
Build context for a specific task.
Args:
task: Description of the task
services: List of service names to search (None = auto-detect)
keywords: Additional keywords to search for
include_graph_hints: Whether to include historical hints from Graphiti
Returns:
TaskContext with relevant files and patterns
"""
# Auto-detect services if not specified
if not services:
services = self.service_matcher.suggest_services(task)
# Extract keywords from task if not provided
if not keywords:
keywords = self.keyword_extractor.extract_keywords(task)
# Search each service
all_matches: list[FileMatch] = []
service_contexts = {}
for service_name in services:
service_info = self.project_index.get("services", {}).get(service_name)
if not service_info:
continue
service_path = Path(service_info.get("path", service_name))
if not service_path.is_absolute():
service_path = self.project_dir / service_path
# Search this service
matches = self.searcher.search_service(service_path, service_name, keywords)
all_matches.extend(matches)
# Load or generate service context
service_contexts[service_name] = self._get_service_context(
service_path, service_name, service_info
)
# Categorize matches
files_to_modify, files_to_reference = self.categorizer.categorize_matches(
all_matches, task
)
# Discover patterns from reference files
patterns = self.pattern_discoverer.discover_patterns(
files_to_reference, keywords
)
# Get graph hints (synchronously wrap async call)
graph_hints = []
if include_graph_hints and is_graphiti_enabled():
try:
# Run the async function in a new event loop if necessary
try:
loop = asyncio.get_running_loop()
# We're already in an async context - this shouldn't happen in CLI
# but handle it gracefully
graph_hints = []
except RuntimeError:
# No event loop running - create one
graph_hints = asyncio.run(
fetch_graph_hints(task, str(self.project_dir))
)
except Exception:
# Graphiti is optional - fail gracefully
graph_hints = []
return TaskContext(
task_description=task,
scoped_services=services,
files_to_modify=[
asdict(f) if isinstance(f, FileMatch) else f for f in files_to_modify
],
files_to_reference=[
asdict(f) if isinstance(f, FileMatch) else f for f in files_to_reference
],
patterns_discovered=patterns,
service_contexts=service_contexts,
graph_hints=graph_hints,
)
async def build_context_async(
self,
task: str,
services: list[str] | None = None,
keywords: list[str] | None = None,
include_graph_hints: bool = True,
) -> TaskContext:
"""
Build context for a specific task (async version).
This version is preferred when called from async code as it can
properly await the graph hints retrieval.
Args:
task: Description of the task
services: List of service names to search (None = auto-detect)
keywords: Additional keywords to search for
include_graph_hints: Whether to include historical hints from Graphiti
Returns:
TaskContext with relevant files and patterns
"""
# Auto-detect services if not specified
if not services:
services = self.service_matcher.suggest_services(task)
# Extract keywords from task if not provided
if not keywords:
keywords = self.keyword_extractor.extract_keywords(task)
# Search each service
all_matches: list[FileMatch] = []
service_contexts = {}
for service_name in services:
service_info = self.project_index.get("services", {}).get(service_name)
if not service_info:
continue
service_path = Path(service_info.get("path", service_name))
if not service_path.is_absolute():
service_path = self.project_dir / service_path
# Search this service
matches = self.searcher.search_service(service_path, service_name, keywords)
all_matches.extend(matches)
# Load or generate service context
service_contexts[service_name] = self._get_service_context(
service_path, service_name, service_info
)
# Categorize matches
files_to_modify, files_to_reference = self.categorizer.categorize_matches(
all_matches, task
)
# Discover patterns from reference files
patterns = self.pattern_discoverer.discover_patterns(
files_to_reference, keywords
)
# Get graph hints asynchronously
graph_hints = []
if include_graph_hints:
graph_hints = await fetch_graph_hints(task, str(self.project_dir))
return TaskContext(
task_description=task,
scoped_services=services,
files_to_modify=[
asdict(f) if isinstance(f, FileMatch) else f for f in files_to_modify
],
files_to_reference=[
asdict(f) if isinstance(f, FileMatch) else f for f in files_to_reference
],
patterns_discovered=patterns,
service_contexts=service_contexts,
graph_hints=graph_hints,
)
def _get_service_context(
self,
service_path: Path,
service_name: str,
service_info: dict,
) -> dict:
"""Get or generate context for a service."""
# Check for SERVICE_CONTEXT.md
context_file = service_path / "SERVICE_CONTEXT.md"
if context_file.exists():
return {
"source": "SERVICE_CONTEXT.md",
"content": context_file.read_text()[:2000], # First 2000 chars
}
# Generate basic context from service info
return {
"source": "generated",
"language": service_info.get("language"),
"framework": service_info.get("framework"),
"type": service_info.get("type"),
"entry_point": service_info.get("entry_point"),
"key_directories": service_info.get("key_directories", {}),
}