Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
8018005
feat: added call-imports metadata
MSami625 Aug 7, 2026
6d46894
feat: added import-calls user metadata
MSami625 Aug 7, 2026
3115b13
feat: implement actor stamping for call import evaluations and user i…
MSami625 Aug 7, 2026
681fe37
feat: enhance user insights with improved call import evaluations
MSami625 Aug 7, 2026
8786a43
chore: add config.docker.yml to .gitignore
MSami625 Aug 7, 2026
55b3bfd
feat: Cache evaluation PDF reports by content fingerprint and reuse S…
MSami625 Aug 7, 2026
7579e28
refactor: update tests for new principal handling
MSami625 Aug 10, 2026
b3f5609
test: add is_enabled method to fake S3 for PDF report branding logo test
MSami625 Aug 10, 2026
ebad2cb
feat: add actor stamping to call import evaluation deletion process
MSami625 Aug 10, 2026
e1281b4
feat: enhance call import evaluation components with inline metadata …
MSami625 Aug 10, 2026
8cf5ec7
fix: update token revocation logic to use correct Redis set parameters
MSami625 Aug 10, 2026
78a664c
feat: implement build_eval_chain_import_apply_async function for impr…
MSami625 Aug 10, 2026
e10fec4
chore: placeholder
MSami625 Aug 11, 2026
0ce2d6a
fix: downloading call import evaluation PDF reports and UI improvments
MSami625 Aug 11, 2026
7e09155
feat: integrate LLM usage tracking and reporting across various compo…
MSami625 Aug 11, 2026
03a7070
Merge branch 'main' into token-usage
MSami625 Aug 12, 2026
f0ebac9
feat: enhance LLM/STT usage tracking with additional context and metr…
MSami625 Aug 12, 2026
16ab94a
Merge branch 'token-usage' of https://github.com/EfficientAI-tech/eff…
MSami625 Aug 12, 2026
09b9793
fix: committed claims management in db
MSami625 Aug 13, 2026
5c51608
refactor: enhanced Redis handling and database transaction integrity
MSami625 Aug 13, 2026
4dd9fdc
refactor: usage commited claims
MSami625 Aug 13, 2026
cbe1c47
feat: usage pricing and integrate usage context across various routes
MSami625 Aug 14, 2026
7d59a5b
feat: add usage flush configuration and enterprise entitlement checks…
MSami625 Aug 14, 2026
cf9547d
refactor: remove Celery beat service and update usage flush configura…
MSami625 Aug 15, 2026
96b0038
Merge remote-tracking branch 'origin/main' into token-usage
MSami625 Aug 15, 2026
468f246
fix: correct indentation in call_imports.py and improve comments in p…
MSami625 Aug 15, 2026
5694c1f
fix(usage): aggregate eval usage per evaluation instead of per record…
MSami625 Aug 15, 2026
a48dc64
feat: add enabled models support for AI providers and new endpoint fo…
MSami625 Aug 15, 2026
cae3bf8
fix(tests): update monkeypatch paths to reflect correct structure
MSami625 Aug 15, 2026
5e4ea48
feat: introduce Celery Beat for platform tasks and update Docker conf…
MSami625 Aug 17, 2026
02e8c0a
fix(migrations): widen source column in model_pricing_rates to VARCHA…
MSami625 Aug 17, 2026
7576d5f
fix(pricing): add clear_rates_table_cache function and update migrati…
MSami625 Aug 17, 2026
83fc098
fix(usage): update recompute flag to false in pricing overrides and u…
MSami625 Aug 17, 2026
49871cb
fix(usage): implement _cost_fields_for_pending_deltas function to str…
MSami625 Aug 17, 2026
a33972d
feat(docs): add usage tracking feature documentation and update relat…
MSami625 Aug 18, 2026
5f5810f
fix(config): update security settings and operational access configur…
MSami625 Aug 20, 2026
cc6292a
fix(tests): enhance test_switch_org_revokes_previous_session_tokens
MSami625 Aug 20, 2026
4d73dc4
Merge origin/main into s-compliance
MSami625 Aug 24, 2026
d6852ab
feat(auth): enhance logout functionality to support access tokens and…
MSami625 Aug 24, 2026
9cf4915
feat(auth): implement revocable session management for user and platf…
MSami625 Aug 24, 2026
d144bb5
fix(auth): improve error handling in session revocation
MSami625 Aug 24, 2026
e8d69d1
feat(usage): enhance LLM usage context management with new tracking f…
MSami625 Aug 25, 2026
5fc705d
feat(flexprice billing): implement tracking for call import evaluatio…
MSami625 Aug 25, 2026
35b69a2
feat(flexprice): add support for flexprice usage metering in various …
MSami625 Aug 25, 2026
5038b0f
refactor(playground): streamline post-call processing by consolidatin…
MSami625 Aug 26, 2026
88d6c45
feat(tests): enhance audio handling in evaluator result tests by intr…
MSami625 Aug 26, 2026
1abaae1
fix(tests): update evaluator and result IDs in bridge evaluator tests…
MSami625 Aug 26, 2026
5347af3
refactor(playground): rename and enhance usage tracking in post-call …
MSami625 Aug 26, 2026
9b4a734
refactor(playground): improve error handling and retry logic in post-…
MSami625 Aug 26, 2026
9f81f8f
feat(flexprice): implement local mocking for Flexprice billing during…
MSami625 Aug 26, 2026
7c277b2
refactor(flexprice): remove unused billing event calls and streamline…
MSami625 Aug 26, 2026
8dc478c
feat(flexprice): add billing support for evaluator recording minutes …
MSami625 Aug 26, 2026
a82a5cc
feat(flexprice): implement repair features functionality to repoint l…
MSami625 Aug 26, 2026
8dc36db
feat(flexprice): enhance subscription management by adding auto-subsc…
MSami625 Aug 26, 2026
5333874
feat(flexprice): add new billing events for agent test setup and prom…
MSami625 Aug 26, 2026
f8916f1
feat(playground): enhance call data processing by merging UI surface …
MSami625 Aug 28, 2026
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: 4 additions & 0 deletions .github/workflows/backend-tests-postgres.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ jobs:
REDIS_URL: redis://localhost:6379/0
CELERY_BROKER_URL: redis://localhost:6379/0
CELERY_RESULT_BACKEND: redis://localhost:6379/0
EFFICIENTAI_PYTEST: "1"
FLEXPRICE_ENABLED: "false"

steps:
- name: Checkout repository
Expand Down Expand Up @@ -83,6 +85,8 @@ jobs:
SHARD_DATABASE_URL_02: postgresql://postgres:postgres@localhost:5432/efficientai_data_02
SHARDING_INTEGRATION_TEST: "1"
REDIS_URL: redis://localhost:6379/0
EFFICIENTAI_PYTEST: "1"
FLEXPRICE_ENABLED: "false"

steps:
- name: Checkout repository
Expand Down
36 changes: 34 additions & 2 deletions app/api/v1/routes/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@
Agents API Routes
Complete CRUD operations for test agents
"""
from fastapi import APIRouter, Depends, HTTPException, status, Query
from uuid import uuid4

from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, status, Query
from fastapi.responses import JSONResponse, Response
from sqlalchemy.orm import Session
from typing import List, Optional
from uuid import UUID
from uuid import UUID, uuid4
import random
from pydantic import BaseModel
from loguru import logger

from app.dependencies import get_db, get_organization_id, get_workspace_id, get_api_key
from app.services.billing.flexprice_service import record_agent_test_setup_generated
from app.models.database import (
Agent, ConversationEvaluation, TestAgentConversation, VoiceBundle,
AIProvider, Integration, IntegrationPlatform, CallMediumEnum,
Expand Down Expand Up @@ -254,6 +257,7 @@ def _scenario_draft_responses(scenarios) -> list[GeneratedScenarioDraftResponse]
@router.post("/generate-test-prompt", response_model=GenerateTestPromptResponse)
async def generate_test_prompt(
data: GenerateTestPromptRequest,
background_tasks: BackgroundTasks,
organization_id: UUID = Depends(get_organization_id),
workspace_id: UUID = Depends(get_workspace_id),
api_key: str = Depends(get_api_key),
Expand Down Expand Up @@ -297,6 +301,14 @@ async def generate_test_prompt(
llm_config=data.llm_config,
credential_id=data.credential_id,
)
background_tasks.add_task(
record_agent_test_setup_generated,
organization_id,
uuid4(),
workspace_id=workspace_id,
purpose="test_prompt",
model=result.model,
)
return GenerateTestPromptResponse(
sections=_test_prompt_section_responses(result.sections),
test_agent_prompt=result.test_agent_prompt,
Expand All @@ -313,6 +325,7 @@ async def generate_test_prompt(
@router.post("/generate-scenarios-from-prompt", response_model=GenerateScenariosFromPromptResponse)
async def generate_scenarios_from_prompt(
data: GenerateScenariosFromPromptRequest,
background_tasks: BackgroundTasks,
organization_id: UUID = Depends(get_organization_id),
workspace_id: UUID = Depends(get_workspace_id),
api_key: str = Depends(get_api_key),
Expand Down Expand Up @@ -357,6 +370,15 @@ async def generate_scenarios_from_prompt(
llm_config=data.llm_config,
credential_id=data.credential_id,
)
background_tasks.add_task(
record_agent_test_setup_generated,
organization_id,
uuid4(),
workspace_id=workspace_id,
purpose="scenarios",
model=result.model,
scenario_count=len(result.scenarios),
)
return GenerateScenariosFromPromptResponse(
scenarios=_scenario_draft_responses(result.scenarios),
provider=result.provider,
Expand All @@ -372,6 +394,7 @@ async def generate_scenarios_from_prompt(
@router.post("/generate-test-setup", response_model=GenerateTestSetupResponse)
async def generate_test_setup(
data: GenerateTestSetupRequest,
background_tasks: BackgroundTasks,
organization_id: UUID = Depends(get_organization_id),
workspace_id: UUID = Depends(get_workspace_id),
api_key: str = Depends(get_api_key),
Expand Down Expand Up @@ -430,6 +453,15 @@ async def generate_test_setup(
llm_config=data.llm_config,
credential_id=data.credential_id,
)
background_tasks.add_task(
record_agent_test_setup_generated,
organization_id,
uuid4(),
workspace_id=workspace_id,
purpose="full_setup",
model=scenario_result.model,
scenario_count=len(scenario_result.scenarios),
)
return GenerateTestSetupResponse(
sections=_test_prompt_section_responses(prompt_result.sections),
test_agent_prompt=prompt_result.test_agent_prompt,
Expand Down
31 changes: 22 additions & 9 deletions app/api/v1/routes/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,18 @@ def _extract_bearer(authorization: Optional[str]) -> Optional[str]:
return token.strip()


def _revoke_local_password_access_token(bearer: str) -> None:
try:
claims = decode_access_token(bearer)
jti = claims.get("jti")
exp = claims.get("exp")
if jti and exp:
ttl = max(int(exp) - int(datetime.now(timezone.utc).timestamp()), 1)
revoke_access_jti(jti, ttl)
except JWTError:
pass


def _issue_session_tokens(
db: Session,
*,
Expand Down Expand Up @@ -611,15 +623,7 @@ def logout(
"""Revoke the current session's refresh token and blacklist the access token."""
bearer = _extract_bearer(authorization)
if bearer and principal.auth_method == AuthMethod.LOCAL_PASSWORD:
try:
claims = decode_access_token(bearer)
jti = claims.get("jti")
exp = claims.get("exp")
if jti and exp:
ttl = max(int(exp) - int(datetime.now(timezone.utc).timestamp()), 1)
revoke_access_jti(jti, ttl)
except JWTError:
pass
_revoke_local_password_access_token(bearer)

if payload and payload.refresh_token:
revoke_refresh_token(db, payload.refresh_token)
Expand Down Expand Up @@ -697,12 +701,14 @@ def refresh_session(payload: RefreshRequest, db: Session = Depends(get_db)) -> T

class SwitchOrgRequest(BaseModel):
organization_id: str
refresh_token: Optional[str] = None


@router.post("/switch-org", response_model=TokenResponse)
def switch_organization(
payload: SwitchOrgRequest,
principal: Principal = Depends(get_principal),
authorization: Optional[str] = Header(None, alias="Authorization"),
db: Session = Depends(get_db),
) -> TokenResponse:
"""
Expand Down Expand Up @@ -766,6 +772,13 @@ def switch_organization(
detail="User is no longer active.",
)

if principal.auth_method == AuthMethod.LOCAL_PASSWORD:
bearer = _extract_bearer(authorization)
if bearer:
_revoke_local_password_access_token(bearer)
if payload.refresh_token:
revoke_refresh_token(db, payload.refresh_token)

user.last_login_at = datetime.now(timezone.utc)
db.commit()
db.refresh(user)
Expand Down
46 changes: 43 additions & 3 deletions app/api/v1/routes/call_import_evaluations.py
Original file line number Diff line number Diff line change
Expand Up @@ -1281,6 +1281,20 @@ def _name_for_source(source: str) -> Optional[str]:
transcribe_overwrite=payload.transcribe_overwrite,
)

from app.services.billing.flexprice_service import (
record_call_import_evaluation_started,
)

for evaluation in created_evaluations:
record_call_import_evaluation_started(
organization_id,
evaluation.id,
workspace_id=evaluation.workspace_id,
call_import_id=call_import.id,
total_rows=int(evaluation.total_rows or 0),
metric_count=len(leaf_metric_ids),
)

for evaluation in created_evaluations:
db.refresh(evaluation)

Expand Down Expand Up @@ -3980,6 +3994,18 @@ async def generate_call_import_evaluation_pdf_report(
detail="Failed to store PDF report due to a concurrent duplicate request.",
) from None
db.refresh(pdf_report)
from app.services.billing.flexprice_service import (
record_call_import_pdf_report_generated,
)

record_call_import_pdf_report_generated(
organization_id,
pdf_report.id,
workspace_id=pdf_report.workspace_id,
evaluation_id=evaluation.id,
call_import_id=call_import_id,
report_type=pdf_report.report_type,
)
return _pdf_report_response_from_row(pdf_report)


Expand Down Expand Up @@ -6926,6 +6952,16 @@ def _resolve_alias(alias_map: Dict[str, str], key: str) -> str:
# in ``app/workers/tasks/helpers/llm_evaluation.py`` — kept local here to
# avoid a worker import cycle from the routes module.
DISCOVERED_METRICS_KEY = "__discovered_metrics__"
METRIC_SCORES_META_KEYS = frozenset({DISCOVERED_METRICS_KEY, "_billing"})


def _is_metric_scores_meta_key(key: str) -> bool:
normalized = str(key or "").strip().lower()
if not normalized:
return True
if normalized in {item.lower() for item in METRIC_SCORES_META_KEYS}:
return True
return normalized.endswith("__discovered")

# Allowed values for an LLM-suggested top-level metric type. Kept in
# sync with ``DiscoveredMetricSuggestedType`` in
Expand Down Expand Up @@ -8674,7 +8710,8 @@ def _reset_eval_row_for_retry(
eval_row.metric_scores = {
key: value
for key, value in existing.items()
if str(key).lower() not in target_keys
if _is_metric_scores_meta_key(str(key))
or str(key).lower() not in target_keys
}
else:
eval_row.metric_scores = {}
Expand Down Expand Up @@ -9181,12 +9218,15 @@ async def retry_call_import_evaluation(
payload.metric_ids if payload else None
)
if metric_ids is not None:
metric_ids = [
mid for mid in metric_ids if not _is_metric_scores_meta_key(str(mid))
]
if not metric_ids:
raise HTTPException(
status_code=400,
detail=(
"metric_ids must be a non-empty list. Omit the "
"field to re-run all metrics."
"metric_ids must be a non-empty list of metric UUIDs. "
"Omit the field to re-run all metrics."
),
)

Expand Down
2 changes: 2 additions & 0 deletions app/api/v1/routes/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class ChatRequest(BaseModel):
temperature: Optional[float] = 0.7
max_tokens: Optional[int] = None
llm_config: Optional[Dict[str, Any]] = None
usage_purpose: Optional[str] = "scenario_description"


class ChatResponse(BaseModel):
Expand Down Expand Up @@ -80,6 +81,7 @@ async def chat_completion(
uuid4(),
workspace_id=workspace_id,
model=result.get("model", request.model),
purpose=request.usage_purpose or "scenario_description",
)

return ChatResponse(
Expand Down
12 changes: 7 additions & 5 deletions app/api/v1/routes/evaluator_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -864,15 +864,17 @@ def re_evaluate_result(
detail="Cannot re-evaluate: this result has no transcription. It must be transcribed first."
)

if not result.evaluator_id:
if not result.evaluator_id and not result.agent_id:
raise HTTPException(
status_code=400,
detail="Cannot re-evaluate: this result is not linked to an evaluator."
detail="Cannot re-evaluate: this result is not linked to an agent or evaluator."
)

evaluator = db.query(Evaluator).filter(Evaluator.id == result.evaluator_id).first()
if not evaluator:
raise HTTPException(status_code=404, detail="Linked evaluator no longer exists")
evaluator = None
if result.evaluator_id:
evaluator = db.query(Evaluator).filter(Evaluator.id == result.evaluator_id).first()
if not evaluator:
raise HTTPException(status_code=404, detail="Linked evaluator no longer exists")

# ------------------------------------------------------------------
# If no audio in S3 yet, try to download from the voice provider
Expand Down
24 changes: 4 additions & 20 deletions app/api/v1/routes/metric_studio.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
MetricStudioRunRetryRequest,
)
from app.services.metric_studio.metric_selection import expand_studio_metric_selection
from app.services.metric_studio.run_rollup import rollup_metric_studio_run
from app.services.metric_studio.source_resolver import resolve_source

router = APIRouter(prefix="/metric-studio", tags=["metric-studio"])
Expand Down Expand Up @@ -117,26 +118,7 @@ def _serialize_result(


def _rollup_run_status(db: Session, run: MetricStudioRun) -> None:
results = (
db.query(MetricStudioRunResult)
.filter(MetricStudioRunResult.run_id == run.id)
.all()
)
completed = sum(1 for r in results if r.status == "completed")
failed = sum(1 for r in results if r.status == "failed")
pending = sum(1 for r in results if r.status in {"pending", "running"})
run.completed_items = completed
run.failed_items = failed
if pending:
run.status = "running"
elif failed and completed:
run.status = "partial"
elif failed:
run.status = "failed"
else:
run.status = "completed"
run.finished_at = datetime.now(timezone.utc)
db.flush()
rollup_metric_studio_run(db, run, emit_flexprice=True, commit=False)


@router.post(
Expand Down Expand Up @@ -297,6 +279,8 @@ def get_metric_studio_run(
)
if not run:
raise HTTPException(status_code=404, detail="Studio run not found.")
rollup_metric_studio_run(db, run, emit_flexprice=True, commit=True)
db.refresh(run)
return _serialize_run(run)


Expand Down
21 changes: 1 addition & 20 deletions app/api/v1/routes/observability.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,11 @@
from typing import Any, Dict, List, Optional, Union
from uuid import UUID

from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, status
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, ConfigDict
from sqlalchemy.orm import Session

from app.dependencies import get_api_key, get_db, get_organization_id, get_workspace_id
from app.services.billing.flexprice_service import (
record_observability_call_evaluated,
record_observability_call_ingested,
)
from app.models.database import (
Agent, APIKey, CallRecording, CallRecordingStatus, CallRecordingSource,
Evaluator, EvaluatorResult, EvaluatorResultStatus, Scenario, Workspace,
Expand Down Expand Up @@ -243,13 +239,6 @@ def _upsert_call_recording(

response = _serialize_call_recording(call_recording, include_data=True, agent=agent_obj)
response["action"] = action
if action == "created":
record_observability_call_ingested(
organization_id,
call_recording.call_short_id,
workspace_id=workspace_id,
provider=provider_platform,
)
return response


Expand Down Expand Up @@ -765,7 +754,6 @@ def _messages_to_speaker_segments(messages: List[Dict[str, Any]]) -> List[Dict[s
async def evaluate_call(
call_short_id: str,
payload: EvaluateCallPayload,
background_tasks: BackgroundTasks,
organization_id: UUID = Depends(get_organization_id),
workspace_id: UUID = Depends(get_workspace_id),
api_key: str = Depends(get_api_key),
Expand Down Expand Up @@ -877,13 +865,6 @@ async def evaluate_call(
except Exception:
pass

background_tasks.add_task(
record_observability_call_evaluated,
organization_id,
call_short_id,
workspace_id=workspace_id,
)

return {
"evaluator_result_id": str(evaluator_result.id),
"result_id": evaluator_result.result_id,
Expand Down
Loading
Loading