Skip to content
Merged
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
55 changes: 45 additions & 10 deletions EfficientAI-Docs/docs/getting-started/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,32 @@ sidebar_position: 1

There are two ways to run the application:

## Method 1: Using Docker Compose
## Method 1: Using Docker Compose (Recommended)

Start all services:
Start all services with a single command:

```bash
docker compose up -d
```

This will automatically:

- Build Docker images if they don't exist
- Build the frontend during the Docker build process
- **Pull pre-built images** from GitHub Container Registry (no build required!)
- Start all services (database, Redis, API, worker)
- Run database migrations automatically on startup

**Note**: If you make changes to the frontend or backend code, you may need to rebuild:
The first run will download ~4GB of images, which typically takes 1-2 minutes depending on your internet speed.

### Using a Specific Version

You can pin to a specific release version for stability:

```bash
# Rebuild and restart (forces rebuild even if image exists)
docker compose up -d --build
# Use a specific version
EFFICIENTAI_VERSION=1.0.0 docker compose up -d

# Or rebuild without cache for a clean build
docker compose build --no-cache api
# Or add to your .env file for persistence
echo "EFFICIENTAI_VERSION=1.0.0" >> .env
docker compose up -d
```

Expand All @@ -57,10 +60,17 @@ docker compose exec api python scripts/create_api_key.py "My API Key"
- Frontend: http://localhost:8000/
- API Docs: http://localhost:8000/docs

**Note**: The frontend is automatically built into the Docker image during the first `docker compose up -d` command. If you make frontend changes later, rebuild with:
### Building Locally (for development)

If you want to build images locally instead of pulling pre-built ones (e.g., for development):

```bash
# Edit docker-compose.yml to uncomment the 'build' sections, then:
docker compose up -d --build

# Or rebuild without cache for a clean build
docker compose build --no-cache api worker
docker compose up -d
```

## Method 2: Using Command Line (CLI)
Expand Down Expand Up @@ -210,9 +220,34 @@ This will:

**For Docker Compose**:
- Docker and Docker Compose installed
- ~4GB disk space for pre-built images

**For CLI**:
- Python 3.11+
- Node.js 18+ and npm
- PostgreSQL running (locally or remote)
- Redis running (locally or remote)

## Docker Images

EfficientAI provides pre-built Docker images hosted on GitHub Container Registry:

| Image | Description | Size |
|-------|-------------|------|
| `ghcr.io/efficientai-tech/efficientai-api` | API server + frontend | ~1.5GB |
| `ghcr.io/efficientai-tech/efficientai-worker` | Celery worker with ML models | ~4GB |

### Available Tags

- `latest` - Most recent build from main branch
- `x.y.z` - Specific version (e.g., `1.0.0`)
- `x.y` - Latest patch of a minor version (e.g., `1.0`)

### Manual Pull (Optional)

Images are pulled automatically by `docker compose up`, but you can pre-pull them:

```bash
docker pull ghcr.io/efficientai-tech/efficientai-api:latest
docker pull ghcr.io/efficientai-tech/efficientai-worker:latest
```
29 changes: 21 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,26 +45,25 @@ Test quality, measure performance, & ship with confidence.

There are two ways to run the application:

### Method 1: Using Docker Compose
### Method 1: Using Docker Compose (Recommended)

1. **Start all services**
```bash
docker compose up -d
```

This will automatically:
- Build Docker images if they don't exist
- Build the frontend during the Docker build process
- Pull pre-built images from GitHub Container Registry (no build required!)
- Start all services (database, Redis, API, worker)
- Run database migrations automatically on startup

**Note:** If you make changes to the frontend or backend code, you may need to rebuild:
**Using a specific version:**
```bash
# Rebuild and restart (forces rebuild even if image exists)
docker compose up -d --build
# Pin to a specific release version
EFFICIENTAI_VERSION=1.0.0 docker compose up -d

# Or rebuild without cache for a clean build
docker compose build --no-cache api worker
# Or add to your .env file
echo "EFFICIENTAI_VERSION=1.0.0" >> .env
docker compose up -d
```

Expand All @@ -81,6 +80,19 @@ There are two ways to run the application:
- Frontend: http://localhost:8000/
- API Docs: http://localhost:8000/docs

#### Building Locally (for development)

If you want to build images locally instead of pulling pre-built ones:

```bash
# Edit docker-compose.yml to uncomment the 'build' sections, then:
docker compose up -d --build

# Or rebuild without cache for a clean build
docker compose build --no-cache api worker
docker compose up -d
```

### Method 2: Using Command Line (CLI)

1. **Install the package**
Expand Down Expand Up @@ -161,6 +173,7 @@ There are two ways to run the application:

**For Docker Compose:**
- Docker and Docker Compose installed
- ~4GB disk space for pre-built images

**For CLI:**
- Python 3.11+
Expand Down
139 changes: 90 additions & 49 deletions app/api/v1/routes/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Complete CRUD operations for test agents
"""
from fastapi import APIRouter, Depends, HTTPException, status, Query
from fastapi.responses import JSONResponse
from fastapi.responses import JSONResponse, Response
from sqlalchemy.orm import Session
from typing import List, Optional
from uuid import UUID
Expand Down Expand Up @@ -147,6 +147,48 @@ def generate_unique_agent_id(db: Session) -> str:
)


def get_agent_dependencies(db: Session, organization_id: UUID, agent_uuid: UUID) -> dict:
"""Return dependency counts that block non-force delete."""
evaluators_count = db.query(Evaluator).filter(
Evaluator.agent_id == agent_uuid,
Evaluator.organization_id == organization_id,
).count()

evaluator_results_count = db.query(EvaluatorResult).filter(
EvaluatorResult.agent_id == agent_uuid,
EvaluatorResult.organization_id == organization_id,
).count()

call_recordings_count = db.query(CallRecording).filter(
CallRecording.agent_id == agent_uuid,
CallRecording.organization_id == organization_id,
).count()

conversation_evaluations_count = db.query(ConversationEvaluation).filter(
ConversationEvaluation.agent_id == agent_uuid,
ConversationEvaluation.organization_id == organization_id,
).count()

test_conversations_count = db.query(TestAgentConversation).filter(
TestAgentConversation.agent_id == agent_uuid,
TestAgentConversation.organization_id == organization_id,
).count()

dependencies = {}
if evaluators_count > 0:
dependencies["evaluators"] = evaluators_count
if evaluator_results_count > 0:
dependencies["evaluator_results"] = evaluator_results_count
if call_recordings_count > 0:
dependencies["call_recordings"] = call_recordings_count
if conversation_evaluations_count > 0:
dependencies["conversation_evaluations"] = conversation_evaluations_count
if test_conversations_count > 0:
dependencies["test_conversations"] = test_conversations_count

return dependencies


@router.post("", response_model=AgentResponse, status_code=status.HTTP_201_CREATED)
async def create_agent(
agent: AgentCreate,
Expand Down Expand Up @@ -380,56 +422,20 @@ async def delete_agent(
raise HTTPException(status_code=404, detail=f"Agent {agent_id} not found")

agent_uuid = db_agent.id

evaluators_count = db.query(Evaluator).filter(
Evaluator.agent_id == agent_uuid,
Evaluator.organization_id == organization_id,
).count()

evaluator_results_count = db.query(EvaluatorResult).filter(
EvaluatorResult.agent_id == agent_uuid,
EvaluatorResult.organization_id == organization_id,
).count()

call_recordings_count = db.query(CallRecording).filter(
CallRecording.agent_id == agent_uuid,
CallRecording.organization_id == organization_id,
).count()

conversation_evaluations_count = db.query(ConversationEvaluation).filter(
ConversationEvaluation.agent_id == agent_uuid,
ConversationEvaluation.organization_id == organization_id,
).count()

test_conversations_count = db.query(TestAgentConversation).filter(
TestAgentConversation.agent_id == agent_uuid,
TestAgentConversation.organization_id == organization_id,
).count()

dependencies = {}
if evaluators_count > 0:
dependencies["evaluators"] = evaluators_count
if evaluator_results_count > 0:
dependencies["evaluator_results"] = evaluator_results_count
if call_recordings_count > 0:
dependencies["call_recordings"] = call_recordings_count
if conversation_evaluations_count > 0:
dependencies["conversation_evaluations"] = conversation_evaluations_count
if test_conversations_count > 0:
dependencies["test_conversations"] = test_conversations_count
dependencies = get_agent_dependencies(db, organization_id, agent_uuid)

if dependencies and not force:
parts = []
if evaluators_count > 0:
parts.append(f"{evaluators_count} evaluator(s)")
if evaluator_results_count > 0:
parts.append(f"{evaluator_results_count} evaluator result(s)")
if call_recordings_count > 0:
parts.append(f"{call_recordings_count} call recording(s)")
if conversation_evaluations_count > 0:
parts.append(f"{conversation_evaluations_count} conversation evaluation(s)")
if test_conversations_count > 0:
parts.append(f"{test_conversations_count} test conversation(s)")
if dependencies.get("evaluators"):
parts.append(f"{dependencies['evaluators']} evaluator(s)")
if dependencies.get("evaluator_results"):
parts.append(f"{dependencies['evaluator_results']} evaluator result(s)")
if dependencies.get("call_recordings"):
parts.append(f"{dependencies['call_recordings']} call recording(s)")
if dependencies.get("conversation_evaluations"):
parts.append(f"{dependencies['conversation_evaluations']} conversation evaluation(s)")
if dependencies.get("test_conversations"):
parts.append(f"{dependencies['test_conversations']} test conversation(s)")

raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
Expand Down Expand Up @@ -479,5 +485,40 @@ async def delete_agent(
},
)

return JSONResponse(status_code=204, content=None)
return Response(status_code=204)


@router.get("/{agent_id}/delete-impact")
async def get_agent_delete_impact(
agent_id: str,
organization_id: UUID = Depends(get_organization_id),
db: Session = Depends(get_db)
):
"""Preview dependent records that would be affected by force delete."""
try:
agent_uuid = UUID(agent_id)
db_agent = db.query(Agent).filter(
and_(
Agent.id == agent_uuid,
Agent.organization_id == organization_id
)
).first()
except ValueError:
db_agent = db.query(Agent).filter(
and_(
Agent.agent_id == agent_id,
Agent.organization_id == organization_id
)
).first()

if not db_agent:
raise HTTPException(status_code=404, detail=f"Agent {agent_id} not found")

dependencies = get_agent_dependencies(db, organization_id, db_agent.id)
return {
"agent_id": str(db_agent.id),
"agent_name": db_agent.name,
"dependencies": dependencies,
"can_delete_without_force": len(dependencies) == 0,
}

7 changes: 3 additions & 4 deletions app/api/v1/routes/conversation_evaluations.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,6 @@ async def create_conversation_evaluation(
2. A brief reason for your answer
3. Additional metrics about the conversation quality:
- Professionalism (0-1 scale)
- Clarity (0-1 scale)
- Empathy (0-1 scale, if applicable)
- Problem Resolution (0-1 scale, if applicable)
- Overall Quality (0-1 scale)
4. An overall score (0.0 to 1.0) representing how well the agent performed
Expand All @@ -97,8 +95,6 @@ async def create_conversation_evaluation(
"objective_achieved_reason": "brief explanation",
"additional_metrics": {{
"professionalism": 0.0-1.0,
"clarity": 0.0-1.0,
"empathy": 0.0-1.0,
"problem_resolution": 0.0-1.0,
"overall_quality": 0.0-1.0
}},
Expand Down Expand Up @@ -151,6 +147,9 @@ async def create_conversation_evaluation(
objective_achieved = bool(evaluation_data.get("objective_achieved", False))
objective_achieved_reason = evaluation_data.get("objective_achieved_reason", "")
additional_metrics = evaluation_data.get("additional_metrics", {})
# Explicitly remove deprecated dimensions so they are not evaluated/stored.
additional_metrics.pop("clarity", None)
additional_metrics.pop("empathy", None)
overall_score = float(evaluation_data.get("overall_score", 0.0))

# Calculate audio metrics if enabled and audio is available
Expand Down
Loading
Loading