|
15 | 15 | from time import time_ns |
16 | 16 | from typing import ( |
17 | 17 | Any, |
| 18 | + AsyncIterator, |
18 | 19 | Callable, |
19 | 20 | Dict, |
| 21 | + Iterator, |
20 | 22 | List, |
21 | 23 | Literal, |
22 | 24 | Optional, |
| 25 | + Sequence, |
23 | 26 | Tuple, |
24 | 27 | Type, |
25 | 28 | Union, |
|
40 | 43 | ) |
41 | 44 | from packaging.version import Version |
42 | 45 |
|
| 46 | +from langfuse._client import pagination |
43 | 47 | from langfuse._client.attributes import ( |
44 | 48 | LangfuseOtelSpanAttributes, |
45 | 49 | _serialize, |
|
107 | 111 | LangfuseAPI, |
108 | 112 | MapValue, |
109 | 113 | NotFoundError, |
| 114 | + ObservationLevel, |
| 115 | + ObservationsV2Response, |
110 | 116 | Prompt_Chat, |
111 | 117 | Prompt_Text, |
112 | 118 | ) |
| 119 | +from langfuse.api.core import RequestOptions |
113 | 120 | from langfuse.api.core.datetime_utils import serialize_datetime |
114 | 121 | from langfuse.batch_evaluation import ( |
115 | 122 | BatchEvaluationResult, |
@@ -444,36 +451,15 @@ def api(self) -> LangfuseAPI: |
444 | 451 | user, session, tags and input/output are on its root observation. Read |
445 | 452 | scores with `api.scores.get_many` and experiments with `api.experiments`. |
446 | 453 |
|
447 | | - - **Read observation pages until `meta.cursor` is absent.** Pages can be |
448 | | - short or empty before the end. The server scans for up to 45 seconds per |
449 | | - request, so pass `request_options={"timeout_in_seconds": 60}`, and pass |
450 | | - `from_start_time` when you know a lower bound (otherwise the server |
451 | | - scans back to 2020-01-01). A 422 means the request hit a server resource |
452 | | - limit: narrow the time range rather than retrying unchanged. A `startTime` |
453 | | - condition in the JSON `filter` overrides `from_start_time` and |
454 | | - `to_start_time`. |
| 454 | + - **Page through observations with `iter_observation_pages`.** Pages can be |
| 455 | + short or empty before the end, and a request can take up to 45 seconds. |
| 456 | + `iter_observation_pages` / `aiter_observation_pages` follow `meta.cursor` |
| 457 | + until it is absent and raise the per-request timeout accordingly. |
455 | 458 |
|
456 | 459 | - For large-scale aggregation (usage/cost by model, user, etc.), prefer the |
457 | 460 | v2 Metrics API (`api.metrics.metrics(...)`) over paginating row-level data. |
458 | 461 |
|
459 | | - Reading all observations of a trace: |
460 | | - ```python |
461 | | - observations, cursor = [], None |
462 | | - while True: |
463 | | - page = langfuse.api.observations.get_many( |
464 | | - trace_id=trace_id, |
465 | | - from_start_time=since, |
466 | | - cursor=cursor, |
467 | | - request_options={"timeout_in_seconds": 60}, |
468 | | - ) |
469 | | - observations.extend(page.data) |
470 | | - cursor = page.meta.cursor |
471 | | - if cursor is None: |
472 | | - break |
473 | | - ``` |
474 | | -
|
475 | | -
|
476 | | - See also: `async_api`, |
| 462 | + See also: `async_api`, `iter_observation_pages`, |
477 | 463 | https://langfuse.com/docs/api-and-data-platform/features/query-via-sdk |
478 | 464 | (ingestion lag: #ingestion-lag), |
479 | 465 | https://langfuse.com/docs/api-and-data-platform/features/observations-api, |
@@ -505,6 +491,164 @@ def async_api(self, value: AsyncLangfuseAPI) -> None: |
505 | 491 |
|
506 | 492 | self._resources.async_api = value |
507 | 493 |
|
| 494 | + def iter_observation_pages( |
| 495 | + self, |
| 496 | + *, |
| 497 | + limit: int = 50, |
| 498 | + cursor: Optional[str] = None, |
| 499 | + max_items: Optional[int] = None, |
| 500 | + fields: Optional[str] = None, |
| 501 | + expand_metadata: Optional[str] = None, |
| 502 | + name: Optional[str] = None, |
| 503 | + user_id: Optional[str] = None, |
| 504 | + session_id: Optional[str] = None, |
| 505 | + type: Optional[str] = None, |
| 506 | + trace_id: Optional[str] = None, |
| 507 | + level: Optional[ObservationLevel] = None, |
| 508 | + parent_observation_id: Optional[str] = None, |
| 509 | + is_root_observation: Optional[bool] = None, |
| 510 | + environment: Optional[Union[str, Sequence[str]]] = None, |
| 511 | + from_start_time: Optional[datetime] = None, |
| 512 | + to_start_time: Optional[datetime] = None, |
| 513 | + version: Optional[str] = None, |
| 514 | + filter: Optional[str] = None, |
| 515 | + request_options: Optional[RequestOptions] = None, |
| 516 | + ) -> Iterator[ObservationsV2Response]: |
| 517 | + """Yield pages of `GET /api/public/v2/observations` until the server returns no cursor. |
| 518 | +
|
| 519 | + A page can be short or empty while more data remains, so only a missing |
| 520 | + `meta.cursor` ends the iteration. Every page is yielded, including empty |
| 521 | + ones. To resume later, save `page.meta.cursor` after processing a page and |
| 522 | + pass it back as `cursor` with the same other arguments. |
| 523 | +
|
| 524 | + Args: |
| 525 | + limit: Rows per request, at most 1000. |
| 526 | + cursor: Cursor to resume from. |
| 527 | + max_items: Stop after this many rows. The last request asks only for the |
| 528 | + remaining rows, so the last page's cursor resumes right after them. |
| 529 | + fields: Comma-separated field groups to include, e.g. `"basic,usage,model"`. |
| 530 | + Defaults to `core` and `basic` on the server. |
| 531 | + expand_metadata: Comma-separated metadata keys to return non-truncated. |
| 532 | + name: Filter by observation name. |
| 533 | + user_id: Filter by user ID. |
| 534 | + session_id: Filter by session ID. |
| 535 | + type: Filter by observation type, e.g. `"GENERATION"`. |
| 536 | + trace_id: Filter by trace ID. |
| 537 | + level: Filter by level, e.g. `"ERROR"`. |
| 538 | + parent_observation_id: Filter by the physical parent observation ID. |
| 539 | + is_root_observation: Filter by whether an observation is a logical root. |
| 540 | + environment: Filter by one or more environments. |
| 541 | + from_start_time: Only observations starting at or after this datetime. |
| 542 | + to_start_time: Only observations starting before this datetime. |
| 543 | + version: Filter by version. |
| 544 | + filter: JSON array of filter conditions. Takes precedence over the |
| 545 | + individual filter arguments; see `api.observations.get_many`. |
| 546 | + request_options: Passed to every request. `timeout_in_seconds` defaults |
| 547 | + to 60 because the server may scan for up to 45 s per request. |
| 548 | +
|
| 549 | + Errors propagate and end the iteration. A 422 means the server hit a resource |
| 550 | + limit, usually its time budget; the same request tends to fail again, so |
| 551 | + narrow the time range or add filters instead of retrying it unchanged. |
| 552 | +
|
| 553 | + Without a lower time bound the server searches back only to 2020-01-01. A |
| 554 | + `startTime` condition in `filter` makes the server ignore `from_start_time` |
| 555 | + and `to_start_time`, so put both bounds in `filter`. Rows are not |
| 556 | + deduplicated; dedupe by `id` if you need unique observations. |
| 557 | +
|
| 558 | + Example: |
| 559 | + ```python |
| 560 | + from datetime import datetime, timedelta, timezone |
| 561 | +
|
| 562 | + from langfuse import get_client |
| 563 | +
|
| 564 | + langfuse = get_client() |
| 565 | + since = datetime.now(timezone.utc) - timedelta(days=7) |
| 566 | +
|
| 567 | + for page in langfuse.iter_observation_pages( |
| 568 | + trace_id="abc123", from_start_time=since, limit=1000 |
| 569 | + ): |
| 570 | + for observation in page.data: |
| 571 | + print(observation.id, observation.name) |
| 572 | + ``` |
| 573 | + """ |
| 574 | + return pagination.iter_observation_pages( |
| 575 | + self.api, |
| 576 | + limit=limit, |
| 577 | + cursor=cursor, |
| 578 | + max_items=max_items, |
| 579 | + request_options=request_options, |
| 580 | + fields=fields, |
| 581 | + expand_metadata=expand_metadata, |
| 582 | + name=name, |
| 583 | + user_id=user_id, |
| 584 | + session_id=session_id, |
| 585 | + type=type, |
| 586 | + trace_id=trace_id, |
| 587 | + level=level, |
| 588 | + parent_observation_id=parent_observation_id, |
| 589 | + is_root_observation=is_root_observation, |
| 590 | + environment=environment, |
| 591 | + from_start_time=from_start_time, |
| 592 | + to_start_time=to_start_time, |
| 593 | + version=version, |
| 594 | + filter=filter, |
| 595 | + ) |
| 596 | + |
| 597 | + def aiter_observation_pages( |
| 598 | + self, |
| 599 | + *, |
| 600 | + limit: int = 50, |
| 601 | + cursor: Optional[str] = None, |
| 602 | + max_items: Optional[int] = None, |
| 603 | + fields: Optional[str] = None, |
| 604 | + expand_metadata: Optional[str] = None, |
| 605 | + name: Optional[str] = None, |
| 606 | + user_id: Optional[str] = None, |
| 607 | + session_id: Optional[str] = None, |
| 608 | + type: Optional[str] = None, |
| 609 | + trace_id: Optional[str] = None, |
| 610 | + level: Optional[ObservationLevel] = None, |
| 611 | + parent_observation_id: Optional[str] = None, |
| 612 | + is_root_observation: Optional[bool] = None, |
| 613 | + environment: Optional[Union[str, Sequence[str]]] = None, |
| 614 | + from_start_time: Optional[datetime] = None, |
| 615 | + to_start_time: Optional[datetime] = None, |
| 616 | + version: Optional[str] = None, |
| 617 | + filter: Optional[str] = None, |
| 618 | + request_options: Optional[RequestOptions] = None, |
| 619 | + ) -> AsyncIterator[ObservationsV2Response]: |
| 620 | + """Async version of `iter_observation_pages`, using `async_api`. |
| 621 | +
|
| 622 | + Example: |
| 623 | + ```python |
| 624 | + async for page in langfuse.aiter_observation_pages(trace_id="abc123"): |
| 625 | + for observation in page.data: |
| 626 | + print(observation.id, observation.name) |
| 627 | + ``` |
| 628 | + """ |
| 629 | + return pagination.aiter_observation_pages( |
| 630 | + self.async_api, |
| 631 | + limit=limit, |
| 632 | + cursor=cursor, |
| 633 | + max_items=max_items, |
| 634 | + request_options=request_options, |
| 635 | + fields=fields, |
| 636 | + expand_metadata=expand_metadata, |
| 637 | + name=name, |
| 638 | + user_id=user_id, |
| 639 | + session_id=session_id, |
| 640 | + type=type, |
| 641 | + trace_id=trace_id, |
| 642 | + level=level, |
| 643 | + parent_observation_id=parent_observation_id, |
| 644 | + is_root_observation=is_root_observation, |
| 645 | + environment=environment, |
| 646 | + from_start_time=from_start_time, |
| 647 | + to_start_time=to_start_time, |
| 648 | + version=version, |
| 649 | + filter=filter, |
| 650 | + ) |
| 651 | + |
508 | 652 | @overload |
509 | 653 | def start_observation( |
510 | 654 | self, |
|
0 commit comments