firecrawl-py 4.10.5__tar.gz → 4.12.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of firecrawl-py might be problematic. Click here for more details.

Files changed (98) hide show
  1. {firecrawl_py-4.10.5/firecrawl_py.egg-info → firecrawl_py-4.12.0}/PKG-INFO +1 -1
  2. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__init__.py +1 -1
  3. firecrawl_py-4.12.0/firecrawl/__tests__/unit/v2/methods/test_agent.py +367 -0
  4. firecrawl_py-4.12.0/firecrawl/__tests__/unit/v2/methods/test_agent_request_preparation.py +226 -0
  5. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/client.py +20 -0
  6. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/v2/client.py +91 -0
  7. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/v2/client_async.py +60 -0
  8. firecrawl_py-4.12.0/firecrawl/v2/methods/agent.py +144 -0
  9. firecrawl_py-4.12.0/firecrawl/v2/methods/aio/agent.py +137 -0
  10. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/v2/types.py +12 -0
  11. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0/firecrawl_py.egg-info}/PKG-INFO +1 -1
  12. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl_py.egg-info/SOURCES.txt +5 -0
  13. firecrawl_py-4.12.0/tests/test_agent_integration.py +277 -0
  14. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/LICENSE +0 -0
  15. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/README.md +0 -0
  16. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/e2e/v2/aio/conftest.py +0 -0
  17. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/e2e/v2/aio/test_aio_batch_scrape.py +0 -0
  18. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/e2e/v2/aio/test_aio_crawl.py +0 -0
  19. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/e2e/v2/aio/test_aio_extract.py +0 -0
  20. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/e2e/v2/aio/test_aio_map.py +0 -0
  21. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/e2e/v2/aio/test_aio_scrape.py +0 -0
  22. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/e2e/v2/aio/test_aio_search.py +0 -0
  23. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/e2e/v2/aio/test_aio_usage.py +0 -0
  24. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/e2e/v2/aio/test_aio_watcher.py +0 -0
  25. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/e2e/v2/conftest.py +0 -0
  26. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/e2e/v2/test_async.py +0 -0
  27. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/e2e/v2/test_batch_scrape.py +0 -0
  28. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/e2e/v2/test_crawl.py +0 -0
  29. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/e2e/v2/test_extract.py +0 -0
  30. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/e2e/v2/test_map.py +0 -0
  31. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/e2e/v2/test_scrape.py +0 -0
  32. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/e2e/v2/test_search.py +0 -0
  33. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/e2e/v2/test_usage.py +0 -0
  34. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/e2e/v2/test_watcher.py +0 -0
  35. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/unit/test_recursive_schema_v1.py +0 -0
  36. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/unit/v2/methods/aio/test_aio_crawl_params.py +0 -0
  37. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/unit/v2/methods/aio/test_aio_crawl_request_preparation.py +0 -0
  38. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/unit/v2/methods/aio/test_aio_crawl_validation.py +0 -0
  39. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/unit/v2/methods/aio/test_aio_map_request_preparation.py +0 -0
  40. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/unit/v2/methods/aio/test_aio_scrape_request_preparation.py +0 -0
  41. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/unit/v2/methods/aio/test_aio_search_request_preparation.py +0 -0
  42. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/unit/v2/methods/aio/test_batch_request_preparation_async.py +0 -0
  43. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/unit/v2/methods/aio/test_ensure_async.py +0 -0
  44. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/unit/v2/methods/test_batch_request_preparation.py +0 -0
  45. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/unit/v2/methods/test_branding.py +0 -0
  46. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/unit/v2/methods/test_crawl_params.py +0 -0
  47. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/unit/v2/methods/test_crawl_request_preparation.py +0 -0
  48. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/unit/v2/methods/test_crawl_validation.py +0 -0
  49. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/unit/v2/methods/test_map_request_preparation.py +0 -0
  50. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/unit/v2/methods/test_pagination.py +0 -0
  51. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/unit/v2/methods/test_scrape_request_preparation.py +0 -0
  52. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/unit/v2/methods/test_search_request_preparation.py +0 -0
  53. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/unit/v2/methods/test_search_validation.py +0 -0
  54. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/unit/v2/methods/test_usage_types.py +0 -0
  55. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/unit/v2/methods/test_webhook.py +0 -0
  56. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/unit/v2/utils/test_metadata_extras.py +0 -0
  57. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/unit/v2/utils/test_metadata_extras_multivalue.py +0 -0
  58. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/unit/v2/utils/test_recursive_schema.py +0 -0
  59. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/unit/v2/utils/test_validation.py +0 -0
  60. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/__tests__/unit/v2/watcher/test_ws_watcher.py +0 -0
  61. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/firecrawl.backup.py +0 -0
  62. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/types.py +0 -0
  63. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/v1/__init__.py +0 -0
  64. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/v1/client.py +0 -0
  65. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/v2/__init__.py +0 -0
  66. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/v2/methods/aio/__init__.py +0 -0
  67. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/v2/methods/aio/batch.py +0 -0
  68. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/v2/methods/aio/crawl.py +0 -0
  69. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/v2/methods/aio/extract.py +0 -0
  70. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/v2/methods/aio/map.py +0 -0
  71. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/v2/methods/aio/scrape.py +0 -0
  72. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/v2/methods/aio/search.py +0 -0
  73. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/v2/methods/aio/usage.py +0 -0
  74. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/v2/methods/batch.py +0 -0
  75. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/v2/methods/crawl.py +0 -0
  76. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/v2/methods/extract.py +0 -0
  77. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/v2/methods/map.py +0 -0
  78. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/v2/methods/scrape.py +0 -0
  79. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/v2/methods/search.py +0 -0
  80. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/v2/methods/usage.py +0 -0
  81. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/v2/utils/__init__.py +0 -0
  82. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/v2/utils/error_handler.py +0 -0
  83. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/v2/utils/get_version.py +0 -0
  84. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/v2/utils/http_client.py +0 -0
  85. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/v2/utils/http_client_async.py +0 -0
  86. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/v2/utils/normalize.py +0 -0
  87. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/v2/utils/validation.py +0 -0
  88. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/v2/watcher.py +0 -0
  89. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl/v2/watcher_async.py +0 -0
  90. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl_py.egg-info/dependency_links.txt +0 -0
  91. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl_py.egg-info/requires.txt +0 -0
  92. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/firecrawl_py.egg-info/top_level.txt +0 -0
  93. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/pyproject.toml +0 -0
  94. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/setup.cfg +0 -0
  95. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/setup.py +0 -0
  96. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/tests/test_api_key_handling.py +0 -0
  97. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/tests/test_change_tracking.py +0 -0
  98. {firecrawl_py-4.10.5 → firecrawl_py-4.12.0}/tests/test_timeout_conversion.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: firecrawl-py
3
- Version: 4.10.5
3
+ Version: 4.12.0
4
4
  Summary: Python SDK for Firecrawl API
5
5
  Home-page: https://github.com/firecrawl/firecrawl
6
6
  Author: Mendable.ai
@@ -17,7 +17,7 @@ from .v1 import (
17
17
  V1ChangeTrackingOptions,
18
18
  )
19
19
 
20
- __version__ = "4.10.5"
20
+ __version__ = "4.12.0"
21
21
 
22
22
  # Define the logger for the Firecrawl project
23
23
  logger: logging.Logger = logging.getLogger("firecrawl")
@@ -0,0 +1,367 @@
1
+ """
2
+ Unit tests for agent methods with mocked HTTP client.
3
+ """
4
+
5
+ import pytest
6
+ import time
7
+ from unittest.mock import Mock, patch
8
+ from pydantic import BaseModel, Field
9
+ from typing import List, Optional
10
+
11
+ from firecrawl.v2.methods.agent import (
12
+ start_agent,
13
+ agent,
14
+ get_agent_status,
15
+ cancel_agent,
16
+ wait_agent
17
+ )
18
+ from firecrawl.v2.types import AgentResponse
19
+ from firecrawl.v2.utils.error_handler import BadRequestError
20
+
21
+
22
+ class TestAgentMethods:
23
+ """Unit tests for agent methods with mocked HTTP client."""
24
+
25
+ def setup_method(self):
26
+ """Set up test fixtures."""
27
+ self.mock_client = Mock()
28
+ self.job_id = "test-agent-123"
29
+
30
+ # Sample agent response
31
+ self.sample_response = {
32
+ "success": True,
33
+ "id": self.job_id,
34
+ "status": "completed",
35
+ "data": {
36
+ "founders": [
37
+ {"name": "John Doe", "role": "CEO"},
38
+ {"name": "Jane Smith", "role": "CTO"}
39
+ ]
40
+ },
41
+ "creditsUsed": 10,
42
+ "expiresAt": "2024-01-01T00:00:00Z"
43
+ }
44
+
45
+ def test_start_agent_basic(self):
46
+ """Test starting an agent job with basic parameters."""
47
+ mock_response = Mock()
48
+ mock_response.ok = True
49
+ mock_response.json.return_value = {
50
+ "success": True,
51
+ "id": self.job_id,
52
+ "status": "processing"
53
+ }
54
+
55
+ self.mock_client.post.return_value = mock_response
56
+
57
+ result = start_agent(
58
+ self.mock_client,
59
+ None,
60
+ prompt="Find information about Firecrawl"
61
+ )
62
+
63
+ # Check that post was called with correct endpoint
64
+ self.mock_client.post.assert_called_once()
65
+ call_args = self.mock_client.post.call_args
66
+ assert call_args[0][0] == "/v2/agent"
67
+
68
+ # Check request body (second positional argument)
69
+ body = call_args[0][1]
70
+ assert body["prompt"] == "Find information about Firecrawl"
71
+ assert "urls" not in body
72
+
73
+ # Check result
74
+ assert isinstance(result, AgentResponse)
75
+ assert result.id == self.job_id
76
+ assert result.status == "processing"
77
+
78
+ def test_start_agent_with_urls(self):
79
+ """Test starting an agent job with URLs."""
80
+ mock_response = Mock()
81
+ mock_response.ok = True
82
+ mock_response.json.return_value = {
83
+ "success": True,
84
+ "id": self.job_id,
85
+ "status": "processing"
86
+ }
87
+
88
+ self.mock_client.post.return_value = mock_response
89
+
90
+ urls = ["https://example.com", "https://test.com"]
91
+ result = start_agent(
92
+ self.mock_client,
93
+ urls,
94
+ prompt="Extract data"
95
+ )
96
+
97
+ call_args = self.mock_client.post.call_args
98
+ body = call_args[0][1]
99
+ assert body["urls"] == urls
100
+
101
+ def test_start_agent_with_dict_schema(self):
102
+ """Test starting an agent job with dict schema."""
103
+ mock_response = Mock()
104
+ mock_response.ok = True
105
+ mock_response.json.return_value = {
106
+ "success": True,
107
+ "id": self.job_id,
108
+ "status": "processing"
109
+ }
110
+
111
+ self.mock_client.post.return_value = mock_response
112
+
113
+ schema = {
114
+ "type": "object",
115
+ "properties": {
116
+ "name": {"type": "string"}
117
+ }
118
+ }
119
+
120
+ result = start_agent(
121
+ self.mock_client,
122
+ None,
123
+ prompt="Extract data",
124
+ schema=schema
125
+ )
126
+
127
+ call_args = self.mock_client.post.call_args
128
+ body = call_args[0][1]
129
+ assert body["schema"] == schema
130
+
131
+ def test_start_agent_with_pydantic_schema(self):
132
+ """Test starting an agent job with Pydantic schema."""
133
+ mock_response = Mock()
134
+ mock_response.ok = True
135
+ mock_response.json.return_value = {
136
+ "success": True,
137
+ "id": self.job_id,
138
+ "status": "processing"
139
+ }
140
+
141
+ self.mock_client.post.return_value = mock_response
142
+
143
+ class Founder(BaseModel):
144
+ name: str = Field(description="Full name")
145
+ role: Optional[str] = Field(None, description="Role")
146
+
147
+ class FoundersSchema(BaseModel):
148
+ founders: List[Founder] = Field(description="List of founders")
149
+
150
+ result = start_agent(
151
+ self.mock_client,
152
+ None,
153
+ prompt="Find founders",
154
+ schema=FoundersSchema
155
+ )
156
+
157
+ call_args = self.mock_client.post.call_args
158
+ body = call_args[0][1]
159
+ assert "schema" in body
160
+ assert body["schema"]["type"] == "object"
161
+ assert "founders" in body["schema"]["properties"]
162
+
163
+ def test_start_agent_with_all_params(self):
164
+ """Test starting an agent job with all parameters."""
165
+ mock_response = Mock()
166
+ mock_response.ok = True
167
+ mock_response.json.return_value = {
168
+ "success": True,
169
+ "id": self.job_id,
170
+ "status": "processing"
171
+ }
172
+
173
+ self.mock_client.post.return_value = mock_response
174
+
175
+ schema = {"type": "object"}
176
+ urls = ["https://example.com"]
177
+
178
+ result = start_agent(
179
+ self.mock_client,
180
+ urls,
181
+ prompt="Complete test",
182
+ schema=schema,
183
+ integration="test-integration",
184
+ max_credits=50,
185
+ strict_constrain_to_urls=True
186
+ )
187
+
188
+ call_args = self.mock_client.post.call_args
189
+ body = call_args[0][1]
190
+ assert body["prompt"] == "Complete test"
191
+ assert body["urls"] == urls
192
+ assert body["schema"] == schema
193
+ assert body["integration"] == "test-integration"
194
+ assert body["maxCredits"] == 50
195
+ assert body["strictConstrainToURLs"] is True
196
+
197
+ def test_get_agent_status(self):
198
+ """Test getting agent status."""
199
+ mock_response = Mock()
200
+ mock_response.ok = True
201
+ mock_response.json.return_value = self.sample_response
202
+
203
+ self.mock_client.get.return_value = mock_response
204
+
205
+ result = get_agent_status(self.mock_client, self.job_id)
206
+
207
+ # Check that get was called with correct endpoint
208
+ self.mock_client.get.assert_called_once_with(f"/v2/agent/{self.job_id}")
209
+
210
+ # Check result
211
+ assert isinstance(result, AgentResponse)
212
+ assert result.id == self.job_id
213
+ assert result.status == "completed"
214
+
215
+ def test_cancel_agent(self):
216
+ """Test canceling an agent job."""
217
+ mock_response = Mock()
218
+ mock_response.ok = True
219
+ mock_response.json.return_value = {"success": True}
220
+
221
+ self.mock_client.delete.return_value = mock_response
222
+
223
+ result = cancel_agent(self.mock_client, self.job_id)
224
+
225
+ # Check that delete was called with correct endpoint
226
+ self.mock_client.delete.assert_called_once_with(f"/v2/agent/{self.job_id}")
227
+
228
+ # Check result
229
+ assert result is True
230
+
231
+ @patch('time.sleep')
232
+ def test_wait_agent_completed(self, mock_sleep):
233
+ """Test waiting for agent to complete."""
234
+ # First call returns processing, second returns completed
235
+ mock_response_processing = Mock()
236
+ mock_response_processing.ok = True
237
+ mock_response_processing.json.return_value = {
238
+ "success": True,
239
+ "id": self.job_id,
240
+ "status": "processing"
241
+ }
242
+
243
+ mock_response_completed = Mock()
244
+ mock_response_completed.ok = True
245
+ mock_response_completed.json.return_value = self.sample_response
246
+
247
+ self.mock_client.get.side_effect = [
248
+ mock_response_processing,
249
+ mock_response_completed
250
+ ]
251
+
252
+ result = wait_agent(self.mock_client, self.job_id, poll_interval=1)
253
+
254
+ # Should have called get twice
255
+ assert self.mock_client.get.call_count == 2
256
+ assert result.status == "completed"
257
+ assert mock_sleep.call_count == 1
258
+
259
+ @patch('time.sleep')
260
+ def test_wait_agent_timeout(self, mock_sleep):
261
+ """Test waiting for agent with timeout."""
262
+ mock_response = Mock()
263
+ mock_response.ok = True
264
+ mock_response.json.return_value = {
265
+ "success": True,
266
+ "id": self.job_id,
267
+ "status": "processing"
268
+ }
269
+
270
+ self.mock_client.get.return_value = mock_response
271
+
272
+ # Mock time.time to simulate timeout
273
+ with patch('time.time', side_effect=[0, 0, 5]): # Start at 0, timeout at 5
274
+ result = wait_agent(
275
+ self.mock_client,
276
+ self.job_id,
277
+ poll_interval=1,
278
+ timeout=3 # Timeout after 3 seconds
279
+ )
280
+
281
+ # Should return processing status due to timeout
282
+ assert result.status == "processing"
283
+
284
+ @patch('time.sleep')
285
+ def test_agent_complete_flow(self, mock_sleep):
286
+ """Test the complete agent flow (start + wait)."""
287
+ # Mock start_agent response
288
+ mock_start_response = Mock()
289
+ mock_start_response.ok = True
290
+ mock_start_response.json.return_value = {
291
+ "success": True,
292
+ "id": self.job_id,
293
+ "status": "processing"
294
+ }
295
+
296
+ # Mock get_agent_status responses
297
+ mock_status_response = Mock()
298
+ mock_status_response.ok = True
299
+ mock_status_response.json.return_value = self.sample_response
300
+
301
+ self.mock_client.post.return_value = mock_start_response
302
+ self.mock_client.get.return_value = mock_status_response
303
+
304
+ result = agent(
305
+ self.mock_client,
306
+ None,
307
+ prompt="Find information",
308
+ poll_interval=1
309
+ )
310
+
311
+ # Should have called post once and get once
312
+ assert self.mock_client.post.call_count == 1
313
+ assert self.mock_client.get.call_count == 1
314
+
315
+ # Check result
316
+ assert isinstance(result, AgentResponse)
317
+ assert result.status == "completed"
318
+ assert result.data is not None
319
+
320
+ def test_agent_immediate_completion(self):
321
+ """Test agent that completes immediately (no job ID)."""
322
+ mock_response = Mock()
323
+ mock_response.ok = True
324
+ mock_response.json.return_value = {
325
+ "success": True,
326
+ "status": "completed",
327
+ "data": {"result": "done"}
328
+ }
329
+
330
+ self.mock_client.post.return_value = mock_response
331
+
332
+ result = agent(
333
+ self.mock_client,
334
+ None,
335
+ prompt="Quick task"
336
+ )
337
+
338
+ # Should only call post, not get
339
+ assert self.mock_client.post.call_count == 1
340
+ assert self.mock_client.get.call_count == 0
341
+ assert result.status == "completed"
342
+
343
+ def test_start_agent_error_handling(self):
344
+ """Test error handling in start_agent."""
345
+ mock_response = Mock()
346
+ mock_response.ok = False
347
+ mock_response.status_code = 400
348
+ mock_response.text = "Bad Request"
349
+ # Mock response.json() to return error details
350
+ mock_response.json.return_value = {
351
+ "error": "Invalid request",
352
+ "details": "Bad Request"
353
+ }
354
+
355
+ self.mock_client.post.return_value = mock_response
356
+
357
+ with pytest.raises(BadRequestError) as exc_info:
358
+ start_agent(
359
+ self.mock_client,
360
+ None,
361
+ prompt="Test prompt"
362
+ )
363
+
364
+ # Verify the exception has the correct status code
365
+ assert exc_info.value.status_code == 400
366
+ assert "agent" in str(exc_info.value).lower()
367
+
@@ -0,0 +1,226 @@
1
+ """
2
+ Unit tests for agent request preparation.
3
+ """
4
+
5
+ import pytest
6
+ from pydantic import BaseModel, Field
7
+ from typing import List, Optional
8
+
9
+ from firecrawl.v2.methods.agent import _prepare_agent_request
10
+
11
+
12
+ class TestAgentRequestPreparation:
13
+ """Unit tests for agent request preparation."""
14
+
15
+ def test_basic_request_preparation(self):
16
+ """Test basic request preparation with minimal fields."""
17
+ data = _prepare_agent_request(
18
+ None,
19
+ prompt="Find information about Firecrawl"
20
+ )
21
+
22
+ assert data["prompt"] == "Find information about Firecrawl"
23
+ assert "urls" not in data
24
+ assert "schema" not in data
25
+
26
+ def test_request_with_urls(self):
27
+ """Test request preparation with URLs."""
28
+ urls = ["https://example.com", "https://test.com"]
29
+ data = _prepare_agent_request(
30
+ urls,
31
+ prompt="Extract data from these pages"
32
+ )
33
+
34
+ assert data["prompt"] == "Extract data from these pages"
35
+ assert data["urls"] == urls
36
+
37
+ def test_request_with_dict_schema(self):
38
+ """Test request preparation with dict schema."""
39
+ schema = {
40
+ "type": "object",
41
+ "properties": {
42
+ "name": {"type": "string"},
43
+ "age": {"type": "integer"}
44
+ }
45
+ }
46
+ data = _prepare_agent_request(
47
+ None,
48
+ prompt="Extract person data",
49
+ schema=schema
50
+ )
51
+
52
+ assert data["prompt"] == "Extract person data"
53
+ assert data["schema"] == schema
54
+
55
+ def test_request_with_pydantic_schema(self):
56
+ """Test request preparation with Pydantic BaseModel schema."""
57
+ class Person(BaseModel):
58
+ name: str = Field(description="Person's name")
59
+ age: Optional[int] = Field(None, description="Person's age")
60
+
61
+ data = _prepare_agent_request(
62
+ None,
63
+ prompt="Extract person data",
64
+ schema=Person
65
+ )
66
+
67
+ assert data["prompt"] == "Extract person data"
68
+ assert "schema" in data
69
+ assert data["schema"]["type"] == "object"
70
+ assert "properties" in data["schema"]
71
+ assert "name" in data["schema"]["properties"]
72
+ assert "age" in data["schema"]["properties"]
73
+
74
+ def test_request_with_pydantic_schema_instance(self):
75
+ """Test request preparation with Pydantic model instance."""
76
+ class Person(BaseModel):
77
+ name: str = Field(description="Person's name")
78
+ age: Optional[int] = Field(None, description="Person's age")
79
+
80
+ person_instance = Person(name="John", age=30)
81
+ data = _prepare_agent_request(
82
+ None,
83
+ prompt="Extract person data",
84
+ schema=person_instance
85
+ )
86
+
87
+ assert data["prompt"] == "Extract person data"
88
+ assert "schema" in data
89
+ # Should use the class schema, not the instance data
90
+ assert data["schema"]["type"] == "object"
91
+
92
+ def test_request_with_nested_pydantic_schema(self):
93
+ """Test request preparation with nested Pydantic schema."""
94
+ class Founder(BaseModel):
95
+ name: str = Field(description="Full name of the founder")
96
+ role: Optional[str] = Field(None, description="Role or position")
97
+
98
+ class FoundersSchema(BaseModel):
99
+ founders: List[Founder] = Field(description="List of founders")
100
+
101
+ data = _prepare_agent_request(
102
+ None,
103
+ prompt="Find the founders",
104
+ schema=FoundersSchema
105
+ )
106
+
107
+ assert data["prompt"] == "Find the founders"
108
+ assert "schema" in data
109
+ assert data["schema"]["type"] == "object"
110
+ assert "founders" in data["schema"]["properties"]
111
+ assert data["schema"]["properties"]["founders"]["type"] == "array"
112
+
113
+ def test_request_with_integration(self):
114
+ """Test request preparation with integration tag."""
115
+ data = _prepare_agent_request(
116
+ None,
117
+ prompt="Test prompt",
118
+ integration=" test-integration "
119
+ )
120
+
121
+ assert data["prompt"] == "Test prompt"
122
+ assert data["integration"] == "test-integration"
123
+
124
+ def test_request_with_max_credits(self):
125
+ """Test request preparation with max credits."""
126
+ data = _prepare_agent_request(
127
+ None,
128
+ prompt="Test prompt",
129
+ max_credits=100
130
+ )
131
+
132
+ assert data["prompt"] == "Test prompt"
133
+ assert data["maxCredits"] == 100
134
+
135
+ def test_request_with_strict_constrain_to_urls(self):
136
+ """Test request preparation with strict_constrain_to_urls."""
137
+ data = _prepare_agent_request(
138
+ ["https://example.com"],
139
+ prompt="Test prompt",
140
+ strict_constrain_to_urls=True
141
+ )
142
+
143
+ assert data["prompt"] == "Test prompt"
144
+ assert data["strictConstrainToURLs"] is True
145
+
146
+ def test_request_all_fields(self):
147
+ """Test request preparation with all fields."""
148
+ schema = {
149
+ "type": "object",
150
+ "properties": {"test": {"type": "string"}}
151
+ }
152
+ urls = ["https://example.com"]
153
+
154
+ data = _prepare_agent_request(
155
+ urls,
156
+ prompt="Complete test",
157
+ schema=schema,
158
+ integration="test-integration",
159
+ max_credits=50,
160
+ strict_constrain_to_urls=True
161
+ )
162
+
163
+ assert data["prompt"] == "Complete test"
164
+ assert data["urls"] == urls
165
+ assert data["schema"] == schema
166
+ assert data["integration"] == "test-integration"
167
+ assert data["maxCredits"] == 50
168
+ assert data["strictConstrainToURLs"] is True
169
+
170
+ def test_request_with_empty_integration(self):
171
+ """Test that empty integration is not included."""
172
+ data = _prepare_agent_request(
173
+ None,
174
+ prompt="Test prompt",
175
+ integration=" "
176
+ )
177
+
178
+ assert "integration" not in data
179
+
180
+ def test_request_with_zero_max_credits(self):
181
+ """Test that zero max_credits is not included."""
182
+ data = _prepare_agent_request(
183
+ None,
184
+ prompt="Test prompt",
185
+ max_credits=0
186
+ )
187
+
188
+ assert "maxCredits" not in data
189
+
190
+ def test_request_with_false_strict_constrain(self):
191
+ """Test that False strict_constrain_to_urls is not included."""
192
+ data = _prepare_agent_request(
193
+ None,
194
+ prompt="Test prompt",
195
+ strict_constrain_to_urls=False
196
+ )
197
+
198
+ assert "strictConstrainToURLs" not in data
199
+
200
+ def test_request_with_invalid_schema_type_string(self):
201
+ """Test that invalid schema types raise ValueError."""
202
+ with pytest.raises(ValueError, match="Invalid schema type"):
203
+ _prepare_agent_request(
204
+ None,
205
+ prompt="Test prompt",
206
+ schema="invalid_string_schema"
207
+ )
208
+
209
+ def test_request_with_invalid_schema_type_int(self):
210
+ """Test that invalid schema types raise ValueError."""
211
+ with pytest.raises(ValueError, match="Invalid schema type"):
212
+ _prepare_agent_request(
213
+ None,
214
+ prompt="Test prompt",
215
+ schema=123
216
+ )
217
+
218
+ def test_request_with_invalid_schema_type_list(self):
219
+ """Test that invalid schema types raise ValueError."""
220
+ with pytest.raises(ValueError, match="Invalid schema type"):
221
+ _prepare_agent_request(
222
+ None,
223
+ prompt="Test prompt",
224
+ schema=["not", "a", "valid", "schema"]
225
+ )
226
+
@@ -71,6 +71,11 @@ class V2Proxy:
71
71
  self.start_extract = client_instance.start_extract
72
72
  self.get_extract_status = client_instance.get_extract_status
73
73
 
74
+ self.agent = client_instance.agent
75
+ self.start_agent = client_instance.start_agent
76
+ self.get_agent_status = client_instance.get_agent_status
77
+ self.cancel_agent = client_instance.cancel_agent
78
+
74
79
  self.start_batch_scrape = client_instance.start_batch_scrape
75
80
  self.get_batch_scrape_status = client_instance.get_batch_scrape_status
76
81
  self.cancel_batch_scrape = client_instance.cancel_batch_scrape
@@ -132,6 +137,11 @@ class AsyncV2Proxy:
132
137
  self.start_extract = client_instance.start_extract
133
138
  self.get_extract_status = client_instance.get_extract_status
134
139
 
140
+ self.agent = client_instance.agent
141
+ self.start_agent = client_instance.start_agent
142
+ self.get_agent_status = client_instance.get_agent_status
143
+ self.cancel_agent = client_instance.cancel_agent
144
+
135
145
  self.start_batch_scrape = client_instance.start_batch_scrape
136
146
  self.get_batch_scrape_status = client_instance.get_batch_scrape_status
137
147
  self.cancel_batch_scrape = client_instance.cancel_batch_scrape
@@ -203,6 +213,11 @@ class Firecrawl:
203
213
  self.get_extract_status = self._v2_client.get_extract_status
204
214
  self.extract = self._v2_client.extract
205
215
 
216
+ self.start_agent = self._v2_client.start_agent
217
+ self.get_agent_status = self._v2_client.get_agent_status
218
+ self.cancel_agent = self._v2_client.cancel_agent
219
+ self.agent = self._v2_client.agent
220
+
206
221
  self.get_concurrency = self._v2_client.get_concurrency
207
222
  self.get_credit_usage = self._v2_client.get_credit_usage
208
223
  self.get_token_usage = self._v2_client.get_token_usage
@@ -249,6 +264,11 @@ class AsyncFirecrawl:
249
264
  self.get_extract_status = self._v2_client.get_extract_status
250
265
  self.extract = self._v2_client.extract
251
266
 
267
+ self.start_agent = self._v2_client.start_agent
268
+ self.get_agent_status = self._v2_client.get_agent_status
269
+ self.cancel_agent = self._v2_client.cancel_agent
270
+ self.agent = self._v2_client.agent
271
+
252
272
  self.get_concurrency = self._v2_client.get_concurrency
253
273
  self.get_credit_usage = self._v2_client.get_credit_usage
254
274
  self.get_token_usage = self._v2_client.get_token_usage