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
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 4.2.10 on 2026-01-16 17:36

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("ml", "0025_alter_algorithm_task_type"),
]

operations = [
migrations.AlterField(
model_name="processingservice",
name="endpoint_url",
field=models.CharField(blank=True, max_length=1024, null=True),
),
]
21 changes: 19 additions & 2 deletions ami/ml/models/processing_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class ProcessingService(BaseModel):
name = models.CharField(max_length=255)
description = models.TextField(blank=True)
projects = models.ManyToManyField("main.Project", related_name="processing_services", blank=True)
endpoint_url = models.CharField(max_length=1024)
endpoint_url = models.CharField(max_length=1024, null=True, blank=True)
pipelines = models.ManyToManyField("ml.Pipeline", related_name="processing_services", blank=True)
last_checked = models.DateTimeField(null=True)
last_checked_live = models.BooleanField(null=True)
Expand All @@ -48,7 +48,8 @@ class ProcessingService(BaseModel):
objects = ProcessingServiceManager()

def __str__(self):
return f'#{self.pk} "{self.name}" at {self.endpoint_url}'
endpoint_display = self.endpoint_url or "No endpoint configured"
return f'#{self.pk} "{self.name}" at {endpoint_display}'

class Meta:
verbose_name = "Processing Service"
Expand Down Expand Up @@ -151,6 +152,19 @@ def get_status(self, timeout=90) -> ProcessingServiceStatusResponse:
Args:
timeout: Request timeout in seconds per attempt (default: 90s for serverless cold starts)
"""
# If no endpoint URL is configured, return a no-op response
if self.endpoint_url is None:
return ProcessingServiceStatusResponse(
timestamp=datetime.datetime.now(),
request_successful=False,
server_live=None,
pipelines_online=[],
pipeline_configs=[],
endpoint_url=self.endpoint_url,
error="No endpoint URL configured - service operates in pull mode",
latency=0.0,
)
Comment on lines +155 to +166
Copy link

Copilot AI Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When endpoint_url is None, the method returns early without updating the last_checked, last_checked_live, and last_checked_latency fields. This means services without endpoint URLs will never have these tracking fields updated. Consider updating these fields even for null endpoint services to maintain consistency and allow tracking when the service was last checked, even if it returned a "no endpoint" status. For example, set last_checked to the current time and last_checked_live to False.

Copilot uses AI. Check for mistakes.
Comment on lines +155 to +166
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Treat blank endpoint_url as missing in status/config fetches.

Because blank=True permits "", these checks should cover empty strings to avoid malformed URLs.

💡 Suggested fix
-        if self.endpoint_url is None:
+        if not self.endpoint_url:
             return ProcessingServiceStatusResponse(
                 timestamp=datetime.datetime.now(),
                 request_successful=False,
                 server_live=None,
                 pipelines_online=[],
                 pipeline_configs=[],
                 endpoint_url=self.endpoint_url,
                 error="No endpoint URL configured - service operates in pull mode",
                 latency=0.0,
             )
-        if self.endpoint_url is None:
+        if not self.endpoint_url:
             return []

Also applies to: 232-234

🤖 Prompt for AI Agents
In `@ami/ml/models/processing_service.py` around lines 155 - 166, The
status/config response builders currently only treat self.endpoint_url is None
as missing; update the checks to treat empty strings as missing too (e.g., use a
falsy check like if not self.endpoint_url or explicitly check for ""/None)
wherever you construct ProcessingServiceStatusResponse and the analogous config
response (e.g., ProcessingServiceConfigResponse) so the error/endpoint_url
fields and returned URLs are correct when endpoint_url is blank; apply the same
change to the other response-returning block that performs the endpoint
existence check.


ready_check_url = urljoin(self.endpoint_url, "readyz")
start_time = time.time()
error = None
Expand Down Expand Up @@ -215,6 +229,9 @@ def get_pipeline_configs(self, timeout=6):
Get the pipeline configurations from the processing service.
This can be a long response as it includes the full category map for each algorithm.
"""
if self.endpoint_url is None:
return []

info_url = urljoin(self.endpoint_url, "info")
resp = requests.get(info_url, timeout=timeout)
resp.raise_for_status()
Expand Down
2 changes: 1 addition & 1 deletion ami/ml/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ class ProcessingServiceStatusResponse(pydantic.BaseModel):
error: str | None = None
server_live: bool | None = None
pipelines_online: list[str] = []
endpoint_url: str
endpoint_url: str | None
latency: float


Expand Down
3 changes: 3 additions & 0 deletions ami/ml/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ def check_processing_services_online():
services = ProcessingService.objects.all()

for service in services:
if service.endpoint_url is None:
logger.warning(f"Processing service {service} has no endpoint URL, skipping.")
continue
Comment on lines +112 to +114
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Handle blank endpoint_url as missing too.

blank=True allows empty strings, but the current guard only skips None, so a blank value still attempts a status check and can generate invalid URLs. Consider treating falsy values as missing.

💡 Suggested fix
-        if service.endpoint_url is None:
+        if not service.endpoint_url:
             logger.warning(f"Processing service {service} has no endpoint URL, skipping.")
             continue
🤖 Prompt for AI Agents
In `@ami/ml/tasks.py` around lines 112 - 114, The loop currently only skips
services when service.endpoint_url is None, but blank or whitespace-only strings
should be treated as missing too; update the guard around service.endpoint_url
(the check that logs via logger.warning and continues) to treat
falsy/empty/whitespace values as missing (e.g., use a check like if not
service.endpoint_url or not service.endpoint_url.strip()) so blank endpoint_url
values are skipped the same as None.

logger.info(f"Checking service {service}")
try:
status_response = service.get_status()
Expand Down
45 changes: 45 additions & 0 deletions ami/ml/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,51 @@ def test_processing_service_pipeline_registration(self):

self.assertEqual(pipelines_queryset.count(), len(response["pipelines"]))

def test_create_processing_service_without_endpoint_url(self):
"""Test creating a ProcessingService without endpoint_url (pull mode)"""
processing_services_create_url = reverse_with_params("api:processingservice-list")
self.client.force_authenticate(user=self.user)
processing_service_data = {
"project": self.project.pk,
"name": "Pull Mode Service",
"description": "Service without endpoint",
}
resp = self.client.post(processing_services_create_url, processing_service_data)
self.client.force_authenticate(user=None)

self.assertEqual(resp.status_code, 201)
data = resp.json()

# Check that endpoint_url is null
self.assertIsNone(data["instance"]["endpoint_url"])

# Check that status indicates no endpoint configured
self.assertFalse(data["status"]["request_successful"])
self.assertIn("No endpoint URL configured", data["status"]["error"])
self.assertIsNone(data["status"]["endpoint_url"])

def test_get_status_with_null_endpoint_url(self):
"""Test get_status method when endpoint_url is None"""
service = ProcessingService.objects.create(name="Pull Mode Service", endpoint_url=None)
service.projects.add(self.project)

status = service.get_status()

self.assertFalse(status.request_successful)
self.assertIsNone(status.server_live)
self.assertIsNone(status.endpoint_url)
self.assertIsNotNone(status.error)
self.assertTrue("No endpoint URL configured" in (status.error or ""))
Copy link

Copilot AI Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assertTrue(a in b) cannot provide an informative message. Using assertIn(a, b) instead will give more informative messages.

Suggested change
self.assertTrue("No endpoint URL configured" in (status.error or ""))
self.assertIn("No endpoint URL configured", (status.error or ""))

Copilot uses AI. Check for mistakes.
self.assertEqual(status.pipelines_online, [])

def test_get_pipeline_configs_with_null_endpoint_url(self):
"""Test get_pipeline_configs method when endpoint_url is None"""
service = ProcessingService.objects.create(name="Pull Mode Service", endpoint_url=None)

configs = service.get_pipeline_configs()

self.assertEqual(configs, [])


class TestPipelineWithProcessingService(TestCase):
def test_run_pipeline_with_errors_from_processing_service(self):
Expand Down
1 change: 1 addition & 0 deletions requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ django-anymail[sendgrid]==10.0 # https://github.com/anymail/django-anymail
Werkzeug[watchdog]==2.3.6 # https://github.com/pallets/werkzeug
ipdb==0.13.13 # https://github.com/gotcha/ipdb
psycopg[binary]==3.1.9 # https://github.com/psycopg/psycopg
# psycopg==3.1.9 # https://github.com/psycopg/psycopg # the non-binary version is needed for some platforms
Copy link

Copilot AI Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This commented-out line appears to be unrelated to the pull request's purpose of making the ProcessingService endpoint_url nullable. If this is a legitimate note about platform compatibility, it should be in a separate PR. Otherwise, it should be removed to keep the changes focused.

Suggested change
# psycopg==3.1.9 # https://github.com/psycopg/psycopg # the non-binary version is needed for some platforms

Copilot uses AI. Check for mistakes.
watchfiles==0.19.0 # https://github.com/samuelcolvin/watchfiles

# Testing
Expand Down
Loading