Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ vendor/bin/pest tests/Feature/Commands/KnowledgeSearchCommandTest.php
- **Storage**: Qdrant vector database only (no SQLite, no Eloquent models)
- **Cache**: Redis via KnowledgeCacheService
- **Embeddings**: sentence-transformers via EmbeddingService
- **LLM**: Ollama via OllamaService (optional, for auto-tagging)
- **LLM**: AI via AiService (optional, for auto-tagging)
- **HTTP**: Saloon connectors in `app/Integrations/Qdrant/`
- **Commands**: `app/Commands/` — extend `LaravelZero\Framework\Commands\Command`
- **Services**: `app/Services/` — registered in `app/Providers/AppServiceProvider.php`
Expand Down Expand Up @@ -57,7 +57,7 @@ vendor/bin/pest tests/Feature/Commands/KnowledgeSearchCommandTest.php
| `TieredSearchService` | Narrow-to-wide retrieval across 4 search tiers |
| `ProjectDetectorService` | Auto-detect project namespace from git repo |
| `EnhancementQueueService` | File-based queue for async Ollama auto-tagging |
| `OllamaService` | LLM integration for auto-tagging and query expansion |
| `AiService` | AI integration for auto-tagging and query expansion |
| `PatternDetectorService` | Detect duplicate/similar entries before persistence |

## TDD Workflow
Expand Down
11 changes: 5 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

# Default target
help:
@echo "Knowledge ChromaDB Management"
@echo "Knowledge Vector Database Management"
@echo ""
@echo "Usage:"
@echo " make up - Start ChromaDB and embedding server"
@echo " make up - Start Qdrant and embedding server"
@echo " make down - Stop all services"
@echo " make logs - Tail service logs"
@echo " make status - Check service status"
Expand All @@ -16,18 +16,17 @@ help:

# Start services
up:
@echo "Starting ChromaDB and embedding server..."
@echo "Starting Qdrant and embedding server..."
@docker compose up -d
@echo "Waiting for services to be healthy..."
@sleep 5
@docker compose ps
@echo ""
@echo "Services ready!"
@echo " ChromaDB: http://localhost:8000"
@echo " Qdrant: http://localhost:6333"
@echo " Embeddings: http://localhost:8001"
@echo ""
@echo "Enable in .env:"
@echo " CHROMADB_ENABLED=true"
@echo " SEMANTIC_SEARCH_ENABLED=true"

# Stop services
Expand All @@ -43,7 +42,7 @@ status:
@docker compose ps
@echo ""
@echo "Health checks:"
@curl -sf http://localhost:8000/api/v1/heartbeat > /dev/null && echo " ChromaDB: OK" || echo " ChromaDB: NOT RUNNING"
@curl -sf http://localhost:6333/collections > /dev/null && echo " Qdrant: OK" || echo " Qdrant: NOT RUNNING"
@curl -sf http://localhost:8001/health > /dev/null && echo " Embeddings: OK" || echo " Embeddings: NOT RUNNING"

# Restart services
Expand Down
26 changes: 13 additions & 13 deletions app/Commands/EnhanceWorkerCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace App\Commands;

use App\Services\EnhancementQueueService;
use App\Services\OllamaService;
use App\Services\AiService;
use App\Services\QdrantService;
use LaravelZero\Framework\Commands\Command;

Expand All @@ -20,35 +20,35 @@ class EnhanceWorkerCommand extends Command
{--once : Process one item and exit}
{--status : Show queue status and exit}';

protected $description = 'Process the enhancement queue using Ollama';
protected $description = 'Process the enhancement queue using AI';

public function handle(
EnhancementQueueService $queue,
OllamaService $ollama,
AiService $ai,
QdrantService $qdrant,
): int {
if ((bool) $this->option('status')) {
return $this->showStatus($queue);
}

if (! $ollama->isAvailable()) {
warning('Ollama is not available. Skipping enhancement processing.');
if (! $ai->isAvailable()) {
warning('AI is not available. Skipping enhancement processing.');

return self::SUCCESS;
}

$processOnce = (bool) $this->option('once');

if ($processOnce) {
return $this->processOne($queue, $ollama, $qdrant);
return $this->processOne($queue, $ai, $qdrant);
}

return $this->processAll($queue, $ollama, $qdrant);
return $this->processAll($queue, $ai, $qdrant);
}

private function processOne(
EnhancementQueueService $queue,
OllamaService $ollama,
AiService $ai,
QdrantService $qdrant,
): int {
$item = $queue->dequeue();
Expand All @@ -59,12 +59,12 @@ private function processOne(
return self::SUCCESS;
}

return $this->processItem($item, $queue, $ollama, $qdrant);
return $this->processItem($item, $queue, $ai, $qdrant);
}

private function processAll(
EnhancementQueueService $queue,
OllamaService $ollama,
AiService $ai,
QdrantService $qdrant,
): int {
$pending = $queue->pendingCount();
Expand All @@ -81,7 +81,7 @@ private function processAll(
$failed = 0;

while (($item = $queue->dequeue()) !== null) {
$result = $this->processItem($item, $queue, $ollama, $qdrant);
$result = $this->processItem($item, $queue, $ai, $qdrant);

if ($result === self::SUCCESS) {
$processed++;
Expand All @@ -101,15 +101,15 @@ private function processAll(
private function processItem(
array $item,
EnhancementQueueService $queue,
OllamaService $ollama,
AiService $ai,
QdrantService $qdrant,
): int {
$entryId = $item['entry_id'];
$project = $item['project'];

$this->line("Enhancing entry: {$item['title']}");

$enhancement = $ollama->enhance([
$enhancement = $ai->enhance([
'title' => $item['title'],
'content' => $item['content'],
'category' => $item['category'] ?? null,
Expand Down
240 changes: 240 additions & 0 deletions app/Commands/GenerateDocumentationCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
<?php

declare(strict_types=1);

namespace App\Commands;

use App\Commands\Concerns\ResolvesProject;
use App\Services\DocumentationGeneratorService;
use Illuminate\Support\Facades\File;

use function Laravel\Prompts\error;
use function Laravel\Prompts\info;
use function Laravel\Prompts\multiselect;
use function Laravel\Prompts\outro;
use function Laravel\Prompts\spin;
use function Laravel\Prompts\table;
use function Laravel\Prompts\text;
use function Laravel\Prompts\warning;

use LaravelZero\Framework\Commands\Command;

class GenerateDocumentationCommand extends Command
{
use ResolvesProject;

protected $signature = 'generate:docs
{--topic= : Generate docs for specific topic}
{--format=markdown : Output format (markdown, json)}
{--output= : Output file path}
{--include-drafts : Include draft entries in documentation}
{--limit=100 : Maximum number of entries to include}
{--no-enhance : Skip AI enhancement}
{--api-docs : Include API documentation}
{--architecture-docs : Include architecture documentation}
{--debugging-guide : Include debugging guide}
{--setup-guide : Include setup guide}
{--project= : Override project namespace}
{--global : Generate docs across all projects}';

protected $description = 'Generate comprehensive documentation from knowledge base';

public function handle(DocumentationGeneratorService $docGenerator): int
{
$project = $this->resolveProject();

// Build options array
$options = [
'include_drafts' => $this->option('include-drafts'),
'limit' => (int) $this->option('limit'),
'enhance' => ! $this->option('no-enhance'),
'api_docs' => $this->option('api-docs'),
'architecture_docs' => $this->option('architecture-docs'),
'debugging_guide' => $this->option('debugging-guide'),
'setup_guide' => $this->option('setup-guide'),
];

// If no specific doc types selected, enable all
$hasDocTypes = $options['api_docs'] || $options['architecture_docs'] ||
$options['debugging_guide'] || $options['setup_guide'];

if (! $hasDocTypes) {
$selected = multiselect(
'Select documentation types to generate:',
[
'api_docs' => 'API Documentation',
'architecture_docs' => 'Architecture Overview',
'debugging_guide' => 'Debugging Guide',
'setup_guide' => 'Setup Guide',
],
default: ['api_docs', 'architecture_docs', 'debugging_guide', 'setup_guide']
);

foreach ($selected as $type) {
$options[$type] = true;
}
}

try {
if ($topic = $this->option('topic')) {
$documentation = spin(
fn () => $docGenerator->generateTopicDocumentation($topic, $options, $project),
'Generating topic documentation...'
);
} else {
$documentation = spin(
fn () => $docGenerator->generateProjectDocumentation($options, $project),
'Generating project documentation...'
);
}

// Format output
$format = $this->option('format');
$output = $this->formatOutput($documentation, $format);

// Handle output
if ($outputPath = $this->option('output')) {
$this->ensureDirectoryExists(dirname($outputPath));
File::put($outputPath, $output);
outro("Documentation generated and saved to: {$outputPath}");
} else {
$this->displayOutput($output, $format);
}

// Display summary
$this->displaySummary($documentation);

return self::SUCCESS;

} catch (\Exception $e) {
error('Failed to generate documentation: '.$e->getMessage());

return self::FAILURE;
}
}

/**
* Format documentation output.
*/
private function formatOutput(array $documentation, string $format): string
{
return match ($format) {
'json' => json_encode($documentation, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES),
'markdown' => $this->option('topic')
? $documentation['documentation'] ?? ''
: $this->formatProjectMarkdown($documentation),
default => throw new \InvalidArgumentException("Unsupported format: {$format}"),
};
}

/**
* Format project documentation as markdown.
*/
private function formatProjectMarkdown(array $documentation): string
{
$markdown = "# {$documentation['project']} Documentation\n\n";
$markdown .= "> Generated on {$documentation['generated_at']}\n\n";

if (isset($documentation['context_summary'])) {
$markdown .= "## Context Summary\n\n";
$markdown .= $documentation['context_summary']."\n\n";
}

foreach ($documentation['sections'] as $section => $content) {
$markdown .= '## '.ucwords(str_replace('_', ' ', $section))."\n\n";
$markdown .= $content."\n\n";
}

if (isset($documentation['metadata'])) {
$markdown .= "---\n\n";
$markdown .= "## Metadata\n\n";
foreach ($documentation['metadata'] as $key => $value) {
$markdown .= '- **'.ucwords(str_replace('_', ' ', $key))."**: {$value}\n";
}
$markdown .= "\n";
}

return $markdown;
}

/**
* Display output to console.
*/
private function displayOutput(string $output, string $format): void
{
if ($format === 'json') {
info('Generated Documentation (JSON):');
$this->line($output);
} else {
// For markdown, show a preview and ask to display full content
$lines = explode("\n", $output);
$preview = implode("\n", array_slice($lines, 0, 50));

info('Generated Documentation (Markdown - Preview):');
$this->line($preview);

if (count($lines) > 50) {
$showFull = text(
'Show full documentation? (y/N)',
default: 'n',
required: false
);

if (strtolower($showFull) === 'y' || strtolower($showFull) === 'yes') {
$this->line($output);
}
} else {
$this->line($output);
}
}
}

/**
* Display documentation summary.
*/
private function displaySummary(array $documentation): void
{
if ($this->option('topic')) {
$contextCount = count($documentation['context'] ?? []);
$relatedCount = count($documentation['related_entries'] ?? []);

table(
['Metric', 'Count'],
[
['Context Entries', $contextCount],
['Related Entries', $relatedCount],
]
);
} else {
$sectionsCount = count($documentation['sections'] ?? []);
$metadata = $documentation['metadata'] ?? [];

table(
['Metric', 'Value'],
[
['Documentation Sections', $sectionsCount],
['Knowledge Entries', $metadata['entries_count'] ?? 'Unknown'],
['Project', $documentation['project'] ?? 'Unknown'],
['Repository', $metadata['repository'] ?? 'Unknown'],
]
);
}

if (isset($documentation['metadata'])) {
$gitInfo = $documentation['metadata'];
if (isset($gitInfo['branch']) && isset($gitInfo['commit'])) {
warning("Generated from branch {$gitInfo['branch']} at commit {$gitInfo['commit']}");
}
}
}

/**
* Ensure output directory exists.
*/
private function ensureDirectoryExists(string $directory): void
{
if (! is_dir($directory)) {
mkdir($directory, 0755, true);
}
}
}
Loading
Loading