diff --git a/README.md b/README.md index f5a9f60..5d47a81 100644 --- a/README.md +++ b/README.md @@ -180,7 +180,6 @@ chosen_agent = relevant_agents[0] chosen_agent_offering = chosen_agent.offerings[0] job_id = chosen_agent_offering.initiate_job( service_requirement, - expired_at, evaluator_address ) diff --git a/examples/acp_base/external_evaluation/buyer.py b/examples/acp_base/external_evaluation/buyer.py index 066c837..519cc58 100644 --- a/examples/acp_base/external_evaluation/buyer.py +++ b/examples/acp_base/external_evaluation/buyer.py @@ -1,6 +1,5 @@ import logging import threading -from datetime import datetime, timedelta from typing import Optional from dotenv import load_dotenv @@ -85,8 +84,7 @@ def on_new_task(job: ACPJob, memo_to_sign: Optional[ACPMemo] = None): "": "", "": "", }, - evaluator_address=env.EVALUATOR_AGENT_WALLET_ADDRESS, # evaluator address - expired_at=datetime.now() + timedelta(minutes=3.1) # job expiry duration, minimum 3 minutes + evaluator_address=env.EVALUATOR_AGENT_WALLET_ADDRESS, # evaluator address ) logger.info(f"Job {job_id} initiated") diff --git a/examples/acp_base/polling_mode/buyer.py b/examples/acp_base/polling_mode/buyer.py index 76317ed..acae5f6 100644 --- a/examples/acp_base/polling_mode/buyer.py +++ b/examples/acp_base/polling_mode/buyer.py @@ -1,6 +1,5 @@ import logging import time -from datetime import datetime, timedelta from dotenv import load_dotenv @@ -66,7 +65,6 @@ def buyer(): "": "", }, evaluator_address=env.EVALUATOR_AGENT_WALLET_ADDRESS, # evaluator address - expired_at=datetime.now() + timedelta(minutes=3.1), # job expiry duration, minimum 3 minutes ) logger.info(f"Job {job_id} initiated") diff --git a/examples/acp_base/skip_evaluation/buyer.py b/examples/acp_base/skip_evaluation/buyer.py index 1e3c925..be2d442 100644 --- a/examples/acp_base/skip_evaluation/buyer.py +++ b/examples/acp_base/skip_evaluation/buyer.py @@ -1,6 +1,5 @@ import logging import threading -from datetime import datetime, timedelta from typing import Optional from dotenv import load_dotenv @@ -87,7 +86,6 @@ def on_new_task(job: ACPJob, memo_to_sign: Optional[ACPMemo] = None): "": "", "": "", }, - expired_at=datetime.now() + timedelta(minutes=5), # job expiry duration, minimum 3 minutes ) logger.info(f"Job {job_id} initiated") logger.info("Listening for next steps...") diff --git a/pyproject.toml b/pyproject.toml index e7227f1..798bc46 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "virtuals-acp" -version = "0.3.19" +version = "0.3.20" description = "Agent Commerce Protocol Python SDK by Virtuals" authors = ["Steven Lee Soon Fatt "] readme = "README.md" diff --git a/tests/unit/test_job_offering.py b/tests/unit/test_job_offering.py index b82f2d1..f3193af 100644 --- a/tests/unit/test_job_offering.py +++ b/tests/unit/test_job_offering.py @@ -56,6 +56,7 @@ def basic_offering(self, mock_acp_client, mock_contract_client): name="Test Service", price=10.0, price_type=PriceType.FIXED, + sla_minutes=60, requirement=None, deliverable=None ) @@ -71,6 +72,7 @@ def offering_with_schema(self, mock_acp_client, mock_contract_client): name="Test Service", price=10.0, price_type=PriceType.FIXED, + sla_minutes=60, requirement={ "type": "object", "properties": { @@ -96,6 +98,7 @@ def test_should_initialize_with_required_parameters( name="Test Service", price=10.0, price_type=PriceType.FIXED, + sla_minutes=60, requirement=None, deliverable=None ) @@ -123,6 +126,7 @@ def test_should_initialize_with_optional_parameters( name="Test Service", price=10.0, price_type=PriceType.PERCENTAGE, + sla_minutes=60, requirement=requirement, deliverable=deliverable, ) @@ -145,6 +149,7 @@ def test_should_parse_valid_json_string( name="Test Service", price=10.0, price_type=PriceType.FIXED, + sla_minutes=60, requirement='{"type": "string"}', deliverable=None ) @@ -165,6 +170,7 @@ def test_should_keep_dict_requirement_as_is( name="Test Service", price=10.0, price_type=PriceType.FIXED, + sla_minutes=60, requirement=requirement, deliverable=None ) @@ -195,10 +201,10 @@ class TestInitiateJob: class TestExpiryHandling: """Test expiry date handling""" - def test_should_use_default_expiry_when_none( + def test_should_use_sla_minutes_for_expiry( self, basic_offering, mock_contract_client ): - """Should use default 1 day expiry when not provided""" + """Should use offering sla_minutes for job expiration""" mock_contract_client.get_job_id.return_value = 123 mock_contract_client.handle_operation.return_value = {} basic_offering.acp_client.get_by_client_and_provider.return_value = None @@ -213,35 +219,12 @@ def test_should_use_default_expiry_when_none( service_requirement={"task": "test"} ) - # Check that create_job was called create_call = mock_contract_client.create_job.call_args expired_at = create_call[0][2] # Third positional arg - # Should be 1 day after now - expected = mock_now + timedelta(days=1) + expected = mock_now + timedelta(minutes=basic_offering.sla_minutes) assert expired_at == expected - def test_should_use_custom_expiry_when_provided( - self, basic_offering, mock_contract_client - ): - """Should use custom expiry when provided""" - mock_contract_client.get_job_id.return_value = 123 - mock_contract_client.handle_operation.return_value = {} - basic_offering.acp_client.get_by_client_and_provider.return_value = None - - custom_expiry = datetime( - 2024, 12, 31, 23, 59, 59, tzinfo=timezone.utc) - - basic_offering.initiate_job( - service_requirement={"task": "test"}, - expired_at=custom_expiry - ) - - create_call = mock_contract_client.create_job.call_args - expired_at = create_call[0][2] - - assert expired_at == custom_expiry - class TestServiceRequirementValidation: """Test service requirement validation""" diff --git a/virtuals_acp/client.py b/virtuals_acp/client.py index 4f14b0d..443a1f4 100644 --- a/virtuals_acp/client.py +++ b/virtuals_acp/client.py @@ -315,6 +315,7 @@ def _hydrate_agent(self, agent_data: Dict[str, Any]) -> IACPAgent: price=price, price_type=price_type, required_funds=offering["requiredFunds"], + sla_minutes=offering["slaMinutes"], requirement=offering.get("requirement", None), deliverable=offering.get("deliverable", None), ) diff --git a/virtuals_acp/job_offering.py b/virtuals_acp/job_offering.py index 91ec376..22afbe0 100644 --- a/virtuals_acp/job_offering.py +++ b/virtuals_acp/job_offering.py @@ -30,6 +30,7 @@ class ACPJobOffering(BaseModel): price: float price_type: PriceType required_funds: bool + sla_minutes: int requirement: Optional[Union[Dict[str, Any], str]] = None deliverable: Optional[Union[Dict[str, Any], str]] = None @@ -54,10 +55,8 @@ def initiate_job( self, service_requirement: Union[Dict[str, Any], str], evaluator_address: Optional[str] = None, - expired_at: Optional[datetime] = None, ) -> int: - if expired_at is None: - expired_at = datetime.now(timezone.utc) + timedelta(days=1) + expired_at = datetime.now(timezone.utc) + timedelta(minutes=self.sla_minutes) # Validate against requirement schema if present if self.requirement: @@ -139,7 +138,7 @@ def initiate_job( evaluator_address or self.contract_client.agent_wallet_address, fare_amount.amount, fare_amount.fare.contract_address, - expired_at or datetime.utcnow(), + expired_at, is_x402_job=is_x402_job, )