prismtrace-sdk 0.3.2__py3-none-any.whl

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.
prismtrace/__init__.py ADDED
@@ -0,0 +1,34 @@
1
+ from .client import PRISMtrace
2
+ from .langchain_handler import PRISMtraceCallbackHandler
3
+ from .claude_tracer import ClaudeAgentTracer
4
+ from .langgraph_helper import PRISMtraceLangGraphHandler, wrap_graph as wrap_langgraph
5
+ from .google_adk import PRISMtraceADKAdapter
6
+ from .litellm_callback import install as install_litellm
7
+ from .openai_agents import PRISMtraceTracingProcessor, install as install_openai_agents
8
+ from .elevenlabs_voice import (
9
+ PRISMtraceVoiceTracer,
10
+ install as install_elevenlabs_voice,
11
+ )
12
+
13
+ __version__ = "0.3.2"
14
+ __all__ = [
15
+ # Core
16
+ "PRISMtrace",
17
+ # LangChain (Supported)
18
+ "PRISMtraceCallbackHandler",
19
+ # Anthropic direct (Supported)
20
+ "ClaudeAgentTracer",
21
+ # LangGraph (Supported)
22
+ "PRISMtraceLangGraphHandler",
23
+ "wrap_langgraph",
24
+ # Google ADK (Beta)
25
+ "PRISMtraceADKAdapter",
26
+ # LiteLLM (Universal Gateway, Supported)
27
+ "install_litellm",
28
+ # OpenAI Agents SDK (Preview)
29
+ "PRISMtraceTracingProcessor",
30
+ "install_openai_agents",
31
+ # ElevenLabs Agents — voice (Supported)
32
+ "PRISMtraceVoiceTracer",
33
+ "install_elevenlabs_voice",
34
+ ]
@@ -0,0 +1,363 @@
1
+ """Claude agent tracer — wraps Anthropic client to auto-trace agentic tool use loops.
2
+
3
+ Emits both spans (for the trace detail view) AND a trajectory (for PRISM
4
+ evaluation + trajectory analytics) after each agentic run.
5
+ """
6
+
7
+ import json
8
+ import time
9
+ import uuid
10
+ from datetime import datetime, timezone
11
+ from typing import Any, Callable, Optional
12
+
13
+ import httpx
14
+
15
+
16
+ class ClaudeAgentTracer:
17
+ """Wraps an Anthropic client to trace agentic tool use loops automatically."""
18
+
19
+ def __init__(
20
+ self,
21
+ anthropic_client: Any,
22
+ api_key: str,
23
+ project_id: str,
24
+ endpoint: str = "https://prismtrace-production.up.railway.app",
25
+ agent_name: str = "claude-agent",
26
+ emit_trajectory: bool = True,
27
+ ):
28
+ self.client = anthropic_client
29
+ self.api_key = api_key
30
+ self.project_id = project_id
31
+ self.endpoint = endpoint.rstrip("/")
32
+ self.agent_name = agent_name
33
+ self.emit_trajectory = emit_trajectory
34
+ self._http = httpx.Client(
35
+ headers={
36
+ "x-prismtrace-key": api_key,
37
+ "Content-Type": "application/json",
38
+ },
39
+ timeout=15,
40
+ )
41
+
42
+ def _now_iso(self) -> str:
43
+ return datetime.now(timezone.utc).isoformat()
44
+
45
+ def run(
46
+ self,
47
+ messages: list[dict],
48
+ tools: list[dict],
49
+ system: str = "",
50
+ model: str = "claude-sonnet-4-5-20250514",
51
+ max_iterations: int = 10,
52
+ tool_executor: Optional[Callable[[str, dict], str]] = None,
53
+ session_id: Optional[str] = None,
54
+ ) -> dict:
55
+ """Run the full agentic loop, tracing every step.
56
+
57
+ Emits spans to /api/spans/ingest AND a trajectory to /api/trajectories.
58
+ """
59
+ trace_id = str(uuid.uuid4())
60
+ conversation_id = session_id or str(uuid.uuid4())
61
+ root_span_id = str(uuid.uuid4())
62
+ spans: list[dict] = []
63
+ trajectory_steps: list[dict] = []
64
+ current_messages = list(messages)
65
+ iteration = 0
66
+ total_duration_ms = 0
67
+
68
+ loop_start = self._now_iso()
69
+
70
+ while iteration < max_iterations:
71
+ iteration += 1
72
+ step_span_id = str(uuid.uuid4())
73
+
74
+ # --- LLM call ---
75
+ llm_span_id = str(uuid.uuid4())
76
+ llm_start = self._now_iso()
77
+ t0 = time.perf_counter()
78
+
79
+ kwargs: dict[str, Any] = {
80
+ "model": model,
81
+ "max_tokens": 4096,
82
+ "messages": current_messages,
83
+ "tools": tools,
84
+ }
85
+ if system:
86
+ kwargs["system"] = system
87
+
88
+ response = self.client.messages.create(**kwargs)
89
+ t1 = time.perf_counter()
90
+
91
+ llm_end = self._now_iso()
92
+ llm_duration = (t1 - t0) * 1000
93
+ total_duration_ms += int(llm_duration)
94
+
95
+ usage = getattr(response, "usage", None)
96
+ input_tokens = getattr(usage, "input_tokens", 0) if usage else 0
97
+ output_tokens = getattr(usage, "output_tokens", 0) if usage else 0
98
+
99
+ resp_text_parts = []
100
+ tool_uses = []
101
+ for block in response.content:
102
+ if block.type == "text":
103
+ resp_text_parts.append(block.text)
104
+ elif block.type == "tool_use":
105
+ tool_uses.append(block)
106
+
107
+ spans.append({
108
+ "span_id": llm_span_id,
109
+ "parent_span_id": root_span_id,
110
+ "name": f"llm:{model}",
111
+ "span_type": "llm",
112
+ "input_text": json.dumps(current_messages[-3:]),
113
+ "output_text": "\n".join(resp_text_parts) or None,
114
+ "metadata": {"iteration": iteration},
115
+ "start_time": llm_start,
116
+ "end_time": llm_end,
117
+ "duration_ms": llm_duration,
118
+ "status": "ok",
119
+ "token_count_input": input_tokens,
120
+ "token_count_output": output_tokens,
121
+ "model": model,
122
+ })
123
+
124
+ # Trajectory: record the LLM reasoning step
125
+ step_type = "reasoning" if tool_uses else "final_answer"
126
+ trajectory_steps.append({
127
+ "step_type": step_type,
128
+ "label": f"LLM call (iteration {iteration})",
129
+ "input_summary": (current_messages[-1].get("content", "") or "")[:200]
130
+ if isinstance(current_messages[-1].get("content"), str)
131
+ else f"[{len(current_messages)} messages]",
132
+ "output_summary": (" ".join(resp_text_parts))[:200] or None,
133
+ "duration_ms": int(llm_duration),
134
+ "token_count": input_tokens + output_tokens,
135
+ "status": "success",
136
+ })
137
+
138
+ if response.stop_reason != "tool_use" or not tool_uses:
139
+ break
140
+
141
+ # --- Tool calls ---
142
+ assistant_content = []
143
+ for block in response.content:
144
+ if block.type == "text":
145
+ assistant_content.append({"type": "text", "text": block.text})
146
+ elif block.type == "tool_use":
147
+ assistant_content.append({
148
+ "type": "tool_use",
149
+ "id": block.id,
150
+ "name": block.name,
151
+ "input": block.input,
152
+ })
153
+
154
+ current_messages.append({"role": "assistant", "content": assistant_content})
155
+
156
+ tool_results = []
157
+ for tu in tool_uses:
158
+ tool_span_id = str(uuid.uuid4())
159
+ tool_start = self._now_iso()
160
+ t_tool_0 = time.perf_counter()
161
+
162
+ result_text = ""
163
+ status = "ok"
164
+ error_msg = None
165
+ if tool_executor:
166
+ try:
167
+ result_text = tool_executor(tu.name, tu.input)
168
+ except Exception as e:
169
+ result_text = str(e)
170
+ status = "error"
171
+ error_msg = str(e)
172
+ else:
173
+ result_text = json.dumps({"info": "no tool_executor provided"})
174
+
175
+ t_tool_1 = time.perf_counter()
176
+ tool_end = self._now_iso()
177
+ tool_dur = (t_tool_1 - t_tool_0) * 1000
178
+ total_duration_ms += int(tool_dur)
179
+
180
+ spans.append({
181
+ "span_id": tool_span_id,
182
+ "parent_span_id": root_span_id,
183
+ "name": tu.name,
184
+ "span_type": "tool",
185
+ "input_text": json.dumps(tu.input),
186
+ "output_text": result_text[:10000] if result_text else None,
187
+ "metadata": {"tool_id": tu.id},
188
+ "start_time": tool_start,
189
+ "end_time": tool_end,
190
+ "duration_ms": tool_dur,
191
+ "status": status,
192
+ "error_message": error_msg,
193
+ })
194
+
195
+ # Trajectory: record each tool call
196
+ trajectory_steps.append({
197
+ "step_type": "tool_call",
198
+ "label": tu.name,
199
+ "tool_name": tu.name,
200
+ "input_summary": json.dumps(tu.input)[:200],
201
+ "output_summary": (result_text or "")[:200],
202
+ "duration_ms": int(tool_dur),
203
+ "token_count": 0,
204
+ "status": "success" if status == "ok" else "error",
205
+ })
206
+
207
+ tool_results.append({
208
+ "type": "tool_result",
209
+ "tool_use_id": tu.id,
210
+ "content": result_text,
211
+ })
212
+
213
+ current_messages.append({"role": "user", "content": tool_results})
214
+
215
+ spans.append({
216
+ "span_id": step_span_id,
217
+ "parent_span_id": root_span_id,
218
+ "name": f"agent_step_{iteration}",
219
+ "span_type": "agent",
220
+ "input_text": json.dumps([tu.name for tu in tool_uses]),
221
+ "output_text": None,
222
+ "metadata": {"iteration": iteration, "tool_count": len(tool_uses)},
223
+ "start_time": llm_start,
224
+ "end_time": self._now_iso(),
225
+ "duration_ms": (time.perf_counter() - t0) * 1000,
226
+ "status": "ok",
227
+ })
228
+
229
+ # Root span
230
+ loop_end = self._now_iso()
231
+ spans.insert(0, {
232
+ "span_id": root_span_id,
233
+ "parent_span_id": None,
234
+ "name": "claude_agent_run",
235
+ "span_type": "chain",
236
+ "input_text": json.dumps(messages[:2]),
237
+ "output_text": "\n".join(resp_text_parts) if resp_text_parts else None,
238
+ "metadata": {"total_iterations": iteration, "model": model},
239
+ "start_time": loop_start,
240
+ "end_time": loop_end,
241
+ "duration_ms": None,
242
+ "status": "ok",
243
+ })
244
+
245
+ # Flush spans
246
+ try:
247
+ self._http.post(
248
+ f"{self.endpoint}/api/spans/ingest",
249
+ json={
250
+ "trace_id": trace_id,
251
+ "project_id": self.project_id,
252
+ "spans": spans,
253
+ },
254
+ )
255
+ except Exception as e:
256
+ print(f"PRISMtrace flush error (spans): {e}")
257
+
258
+ # Flush trajectory for PRISM evaluation
259
+ trajectory_id = None
260
+ if self.emit_trajectory and trajectory_steps:
261
+ has_errors = any(s.get("status") == "error" for s in trajectory_steps)
262
+ try:
263
+ resp = self._http.post(
264
+ f"{self.endpoint}/api/trajectories",
265
+ json={
266
+ "project_id": self.project_id,
267
+ "conversation_id": conversation_id,
268
+ "request_id": trace_id,
269
+ "agent_id": self.agent_name,
270
+ "agent_name": self.agent_name,
271
+ "steps": trajectory_steps,
272
+ "total_duration_ms": total_duration_ms,
273
+ "final_status": "error" if has_errors else "success",
274
+ "model": model,
275
+ },
276
+ )
277
+ if resp.status_code in (200, 201):
278
+ trajectory_id = resp.json().get("id")
279
+ except Exception as e:
280
+ print(f"PRISMtrace flush error (trajectory): {e}")
281
+
282
+ return {
283
+ "response": response,
284
+ "trace_id": trace_id,
285
+ "trajectory_id": trajectory_id,
286
+ "iterations": iteration,
287
+ "spans_count": len(spans),
288
+ "trajectory_steps": len(trajectory_steps),
289
+ }
290
+
291
+ def instrument_client(self) -> None:
292
+ """Wraps the Anthropic client's messages.create so every call is auto-traced."""
293
+ original_create = self.client.messages.create
294
+
295
+ tracer = self
296
+
297
+ def traced_create(*args: Any, **kwargs: Any) -> Any:
298
+ trace_id = str(uuid.uuid4())
299
+ span_id = str(uuid.uuid4())
300
+ start = tracer._now_iso()
301
+ t0 = time.perf_counter()
302
+
303
+ response = original_create(*args, **kwargs)
304
+
305
+ t1 = time.perf_counter()
306
+ end = tracer._now_iso()
307
+ dur_ms = (t1 - t0) * 1000
308
+
309
+ usage = getattr(response, "usage", None)
310
+ model = kwargs.get("model", "unknown")
311
+
312
+ tool_spans: list[dict] = []
313
+ if hasattr(response, "content"):
314
+ for block in response.content:
315
+ if hasattr(block, "type") and block.type == "tool_use":
316
+ tool_spans.append({
317
+ "span_id": str(uuid.uuid4()),
318
+ "parent_span_id": span_id,
319
+ "name": block.name,
320
+ "span_type": "tool",
321
+ "input_text": json.dumps(block.input),
322
+ "metadata": {"tool_id": block.id},
323
+ "start_time": start,
324
+ "end_time": end,
325
+ "duration_ms": dur_ms,
326
+ "status": "ok",
327
+ })
328
+
329
+ spans = [
330
+ {
331
+ "span_id": span_id,
332
+ "parent_span_id": None,
333
+ "name": f"llm:{model}",
334
+ "span_type": "llm",
335
+ "input_text": json.dumps(kwargs.get("messages", [])[-3:]),
336
+ "output_text": None,
337
+ "metadata": {},
338
+ "start_time": start,
339
+ "end_time": end,
340
+ "duration_ms": dur_ms,
341
+ "status": "ok",
342
+ "token_count_input": getattr(usage, "input_tokens", None) if usage else None,
343
+ "token_count_output": getattr(usage, "output_tokens", None) if usage else None,
344
+ "model": model,
345
+ },
346
+ *tool_spans,
347
+ ]
348
+
349
+ try:
350
+ tracer._http.post(
351
+ f"{tracer.endpoint}/api/spans/ingest",
352
+ json={
353
+ "trace_id": trace_id,
354
+ "project_id": tracer.project_id,
355
+ "spans": spans,
356
+ },
357
+ )
358
+ except Exception as e:
359
+ print(f"PRISMtrace auto-trace error: {e}")
360
+
361
+ return response
362
+
363
+ self.client.messages.create = traced_create
prismtrace/client.py ADDED
@@ -0,0 +1,265 @@
1
+ """PRISMtrace client for AI observability."""
2
+
3
+ import json
4
+ import sys
5
+ import threading
6
+ import uuid
7
+ from functools import wraps
8
+ from typing import Any, Callable, Optional
9
+
10
+ import httpx
11
+
12
+
13
+ class PRISMtrace:
14
+ def __init__(
15
+ self,
16
+ api_key: str,
17
+ host: str,
18
+ project_id: str,
19
+ timeout: int = 10,
20
+ ):
21
+ self.api_key = api_key
22
+ self.host = host.rstrip("/")
23
+ self.project_id = project_id
24
+ self.timeout = timeout
25
+ self._client = httpx.Client(
26
+ base_url=self.host,
27
+ headers={
28
+ "x-prismtrace-key": api_key,
29
+ "Content-Type": "application/json",
30
+ },
31
+ timeout=self.timeout,
32
+ )
33
+
34
+ # ------------------------------------------------------------------
35
+ # Traces (existing)
36
+ # ------------------------------------------------------------------
37
+
38
+ def trace_llm(
39
+ self,
40
+ model: str,
41
+ input_messages: list,
42
+ output: str,
43
+ latency_ms: int,
44
+ token_count_input: int = 0,
45
+ token_count_output: int = 0,
46
+ trace_id: Optional[str] = None,
47
+ metadata: Optional[dict] = None,
48
+ ) -> None:
49
+ payload = {
50
+ "project_id": self.project_id,
51
+ "model": model,
52
+ "input_messages": input_messages,
53
+ "output_message": output,
54
+ "latency_ms": latency_ms,
55
+ "token_count_input": token_count_input,
56
+ "token_count_output": token_count_output,
57
+ }
58
+ if trace_id is not None:
59
+ payload["trace_id"] = trace_id
60
+ if metadata is not None:
61
+ payload["metadata"] = metadata
62
+ threading.Thread(target=self._post, args=("/api/traces", payload), daemon=True).start()
63
+
64
+ def _post(self, path: str, payload: dict) -> Optional[dict]:
65
+ try:
66
+ resp = self._client.post(path, json=payload)
67
+ if resp.status_code in (200, 201):
68
+ return resp.json()
69
+ print(
70
+ f"PRISMtrace warning: {path} returned {resp.status_code}: {resp.text[:200]}",
71
+ file=sys.stderr,
72
+ )
73
+ except Exception as e:
74
+ print(f"PRISMtrace warning: {path} failed: {e}", file=sys.stderr)
75
+ return None
76
+
77
+ def _post_sync(self, path: str, payload: dict) -> Optional[dict]:
78
+ try:
79
+ resp = self._client.post(path, json=payload)
80
+ if resp.status_code in (200, 201):
81
+ return resp.json()
82
+ print(
83
+ f"PRISMtrace warning: {path} returned {resp.status_code}: {resp.text[:200]}",
84
+ file=sys.stderr,
85
+ )
86
+ except Exception as e:
87
+ print(f"PRISMtrace warning: {path} failed: {e}", file=sys.stderr)
88
+ return None
89
+
90
+ def _get_sync(self, path: str) -> Optional[dict]:
91
+ try:
92
+ resp = self._client.get(path)
93
+ if resp.status_code == 200:
94
+ return resp.json()
95
+ print(
96
+ f"PRISMtrace warning: GET {path} returned {resp.status_code}: {resp.text[:200]}",
97
+ file=sys.stderr,
98
+ )
99
+ except Exception as e:
100
+ print(f"PRISMtrace warning: GET {path} failed: {e}", file=sys.stderr)
101
+ return None
102
+
103
+ def trace(self, name: Optional[str] = None) -> Callable:
104
+ def decorator(fn: Callable) -> Callable:
105
+ @wraps(fn)
106
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
107
+ import time
108
+ start = time.perf_counter()
109
+ result = fn(*args, **kwargs)
110
+ elapsed_ms = int((time.perf_counter() - start) * 1000)
111
+ self.trace_llm(
112
+ model=name or fn.__name__,
113
+ input_messages=[{"role": "user", "content": str(args)}],
114
+ output=str(result),
115
+ latency_ms=elapsed_ms,
116
+ )
117
+ return result
118
+ return wrapper
119
+ return decorator
120
+
121
+ # ------------------------------------------------------------------
122
+ # Trajectories
123
+ # ------------------------------------------------------------------
124
+
125
+ def submit_trajectory(
126
+ self,
127
+ steps: list[dict],
128
+ *,
129
+ agent_name: str = "default-agent",
130
+ agent_id: Optional[str] = None,
131
+ conversation_id: Optional[str] = None,
132
+ request_id: Optional[str] = None,
133
+ model: Optional[str] = None,
134
+ final_status: str = "success",
135
+ async_send: bool = False,
136
+ ) -> Optional[dict]:
137
+ """Submit a trajectory (ordered step list) for PRISM evaluation.
138
+
139
+ Each step dict should contain:
140
+ step_type: "reasoning" | "tool_call" | "final_answer" | ...
141
+ label: short description
142
+ output_summary: what this step produced (optional but recommended)
143
+ tool_name: tool identifier (required when step_type is tool_call)
144
+ input_summary: what went in (optional)
145
+ duration_ms: step latency (optional)
146
+ token_count: tokens used (optional)
147
+ status: "success" | "error" (default "success")
148
+
149
+ Returns the API response dict (trajectory id, step_count, created_at)
150
+ or None on failure.
151
+ """
152
+ payload = {
153
+ "project_id": self.project_id,
154
+ "conversation_id": conversation_id or str(uuid.uuid4()),
155
+ "request_id": request_id or str(uuid.uuid4()),
156
+ "agent_id": agent_id or agent_name,
157
+ "agent_name": agent_name,
158
+ "steps": steps,
159
+ "total_duration_ms": sum(s.get("duration_ms") or 0 for s in steps),
160
+ "final_status": final_status,
161
+ "model": model,
162
+ }
163
+ if async_send:
164
+ threading.Thread(
165
+ target=self._post, args=("/api/trajectories", payload), daemon=True,
166
+ ).start()
167
+ return None
168
+ return self._post_sync("/api/trajectories", payload)
169
+
170
+ def get_trajectory(self, trajectory_id: str) -> Optional[dict]:
171
+ """Fetch a trajectory and its steps."""
172
+ return self._get_sync(f"/api/trajectories/{trajectory_id}")
173
+
174
+ def get_trajectory_evaluation(self, trajectory_id: str) -> Optional[dict]:
175
+ """Fetch PRISM evaluation results for a trajectory."""
176
+ return self._get_sync(f"/api/trajectories/{trajectory_id}/evaluation")
177
+
178
+ def retrigger_evaluation(self, trajectory_id: str) -> Optional[dict]:
179
+ """Re-run PRISM evaluation for an existing trajectory."""
180
+ return self._post_sync(
181
+ f"/api/trajectories/{trajectory_id}/evaluate?project_id={self.project_id}",
182
+ {},
183
+ )
184
+
185
+ # ------------------------------------------------------------------
186
+ # Knowledge Base
187
+ # ------------------------------------------------------------------
188
+
189
+ def kb_upload(
190
+ self,
191
+ filename: str,
192
+ content: bytes | str,
193
+ *,
194
+ agent_id: Optional[str] = None,
195
+ description: Optional[str] = None,
196
+ content_type: str = "text/plain",
197
+ ) -> Optional[dict]:
198
+ """Upload a document to the project knowledge base.
199
+
200
+ Returns {"id", "name", "chunk_count", ...} or None on failure.
201
+ """
202
+ if isinstance(content, str):
203
+ content = content.encode("utf-8")
204
+
205
+ files = {"file": (filename, content, content_type)}
206
+ data: dict[str, str] = {"project_id": self.project_id}
207
+ if agent_id:
208
+ data["agent_id"] = agent_id
209
+ if description:
210
+ data["description"] = description
211
+
212
+ try:
213
+ resp = httpx.post(
214
+ f"{self.host}/api/knowledge-base/documents",
215
+ files=files,
216
+ data=data,
217
+ headers={"x-prismtrace-key": self.api_key},
218
+ timeout=self.timeout,
219
+ )
220
+ if resp.status_code in (200, 201):
221
+ return resp.json()
222
+ print(f"PRISMtrace warning: kb_upload returned {resp.status_code}")
223
+ except Exception as e:
224
+ print(f"PRISMtrace warning: kb_upload failed: {e}")
225
+ return None
226
+
227
+ def kb_search(
228
+ self,
229
+ query: str,
230
+ *,
231
+ agent_id: Optional[str] = None,
232
+ limit: int = 5,
233
+ ) -> list[dict]:
234
+ """Search the project knowledge base. Returns list of chunk results."""
235
+ payload: dict[str, Any] = {
236
+ "query": query,
237
+ "project_id": self.project_id,
238
+ "limit": limit,
239
+ }
240
+ if agent_id:
241
+ payload["agent_id"] = agent_id
242
+ result = self._post_sync("/api/knowledge-base/search", payload)
243
+ if result:
244
+ return result.get("results", [])
245
+ return []
246
+
247
+ def kb_list_documents(self, agent_id: Optional[str] = None) -> list[dict]:
248
+ """List documents in the project knowledge base."""
249
+ path = f"/api/knowledge-base/documents?project_id={self.project_id}"
250
+ if agent_id:
251
+ path += f"&agent_id={agent_id}"
252
+ result = self._get_sync(path)
253
+ if result:
254
+ return result.get("documents", [])
255
+ return []
256
+
257
+ def kb_delete_document(self, doc_id: str) -> bool:
258
+ """Delete a document from the knowledge base."""
259
+ try:
260
+ resp = self._client.delete(
261
+ f"/api/knowledge-base/documents/{doc_id}?project_id={self.project_id}",
262
+ )
263
+ return resp.status_code == 200
264
+ except Exception:
265
+ return False