diffbot-python 0.2.3__tar.gz → 0.3.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.
- diffbot_python-0.3.0/PKG-INFO +54 -0
- diffbot_python-0.3.0/README.md +36 -0
- diffbot_python-0.3.0/pyproject.toml +46 -0
- diffbot_python-0.3.0/src/diffbot/__init__.py +63 -0
- diffbot_python-0.3.0/src/diffbot/ask.py +195 -0
- {diffbot_python-0.2.3 → diffbot_python-0.3.0}/src/diffbot/cli/__init__.py +42 -12
- {diffbot_python-0.2.3 → diffbot_python-0.3.0}/src/diffbot/client.py +68 -7
- {diffbot_python-0.2.3 → diffbot_python-0.3.0}/src/diffbot/kg.py +5 -5
- diffbot_python-0.2.3/PKG-INFO +0 -297
- diffbot_python-0.2.3/README.md +0 -261
- diffbot_python-0.2.3/pyproject.toml +0 -79
- diffbot_python-0.2.3/src/diffbot/__init__.py +0 -38
- diffbot_python-0.2.3/src/diffbot/ask.py +0 -48
- diffbot_python-0.2.3/tests/conftest.py +0 -16
- diffbot_python-0.2.3/tests/test_ask.py +0 -12
- diffbot_python-0.2.3/tests/test_async.py +0 -94
- diffbot_python-0.2.3/tests/test_crawl.py +0 -63
- diffbot_python-0.2.3/tests/test_dql.py +0 -14
- diffbot_python-0.2.3/tests/test_dql_cli.py +0 -118
- diffbot_python-0.2.3/tests/test_extract.py +0 -170
- diffbot_python-0.2.3/tests/test_ontology.py +0 -119
- diffbot_python-0.2.3/tests/test_readme_examples.py +0 -193
- diffbot_python-0.2.3/tests/test_web_search.py +0 -12
- {diffbot_python-0.2.3 → diffbot_python-0.3.0}/.gitignore +0 -0
- {diffbot_python-0.2.3 → diffbot_python-0.3.0}/LICENSE +0 -0
- {diffbot_python-0.2.3 → diffbot_python-0.3.0}/src/diffbot/_auth.py +0 -0
- {diffbot_python-0.2.3 → diffbot_python-0.3.0}/src/diffbot/cli/__main__.py +0 -0
- {diffbot_python-0.2.3 → diffbot_python-0.3.0}/src/diffbot/cli/_common.py +0 -0
- {diffbot_python-0.2.3 → diffbot_python-0.3.0}/src/diffbot/cli/dql.py +0 -0
- {diffbot_python-0.2.3 → diffbot_python-0.3.0}/src/diffbot/cli/entities.py +0 -0
- {diffbot_python-0.2.3 → diffbot_python-0.3.0}/src/diffbot/cli/ontology.py +0 -0
- {diffbot_python-0.2.3 → diffbot_python-0.3.0}/src/diffbot/crawl.py +0 -0
- {diffbot_python-0.2.3 → diffbot_python-0.3.0}/src/diffbot/errors.py +0 -0
- {diffbot_python-0.2.3 → diffbot_python-0.3.0}/src/diffbot/extract.py +0 -0
- {diffbot_python-0.2.3 → diffbot_python-0.3.0}/src/diffbot/nlp.py +0 -0
- {diffbot_python-0.2.3 → diffbot_python-0.3.0}/src/diffbot/ontology.py +0 -0
- {diffbot_python-0.2.3 → diffbot_python-0.3.0}/src/diffbot/web_search.py +0 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: diffbot-python
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: ⚠️ LEGACY — renamed to diffbot. Final release, no further updates: pip install diffbot
|
|
5
|
+
Project-URL: Homepage, https://pypi.org/project/diffbot/
|
|
6
|
+
Project-URL: Repository, https://github.com/diffbot/diffbot-python
|
|
7
|
+
Author-email: Jerome Choo <jerome@diffbot.com>, Mike Tung <miket@diffbot.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 7 - Inactive
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Requires-Dist: click>=8.1.0
|
|
15
|
+
Requires-Dist: httpx>=0.27.0
|
|
16
|
+
Requires-Dist: rich>=13.0.0
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# ⚠️ LEGACY PACKAGE — `diffbot-python` is now `diffbot` ⚠️
|
|
20
|
+
|
|
21
|
+
> ## 🛑 Do not install `diffbot-python`. Install [`diffbot`](https://pypi.org/project/diffbot/) instead:
|
|
22
|
+
>
|
|
23
|
+
> ```bash
|
|
24
|
+
> pip uninstall diffbot-python
|
|
25
|
+
> pip install diffbot
|
|
26
|
+
> ```
|
|
27
|
+
>
|
|
28
|
+
> **This is the final release of `diffbot-python` (0.3.0). It will never be updated again**:
|
|
29
|
+
> no bug fixes, no new Diffbot API features, no security fixes.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
The Diffbot Python library and `db` CLI now live at
|
|
34
|
+
**[diffbot](https://pypi.org/project/diffbot/)**. The import name is unchanged
|
|
35
|
+
(`import diffbot`), so no code changes are needed.
|
|
36
|
+
|
|
37
|
+
This release contains the same code as `diffbot` 3.0.0 so existing installs keep
|
|
38
|
+
working, but it prints a warning on every import until you migrate.
|
|
39
|
+
|
|
40
|
+
## Migrating
|
|
41
|
+
|
|
42
|
+
1. Replace `diffbot-python` with `diffbot` in your `requirements.txt`, `pyproject.toml`, etc.
|
|
43
|
+
2. Uninstall the old package **before** installing the new one:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip uninstall diffbot-python
|
|
47
|
+
pip install diffbot
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Both packages provide the same `diffbot` module, so uninstalling
|
|
51
|
+
`diffbot-python` after `diffbot` is installed deletes files that `diffbot`
|
|
52
|
+
needs. If that happened, run `pip install --force-reinstall diffbot`.
|
|
53
|
+
|
|
54
|
+
Source and issues: <https://github.com/diffbot/diffbot-python>.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# ⚠️ LEGACY PACKAGE — `diffbot-python` is now `diffbot` ⚠️
|
|
2
|
+
|
|
3
|
+
> ## 🛑 Do not install `diffbot-python`. Install [`diffbot`](https://pypi.org/project/diffbot/) instead:
|
|
4
|
+
>
|
|
5
|
+
> ```bash
|
|
6
|
+
> pip uninstall diffbot-python
|
|
7
|
+
> pip install diffbot
|
|
8
|
+
> ```
|
|
9
|
+
>
|
|
10
|
+
> **This is the final release of `diffbot-python` (0.3.0). It will never be updated again**:
|
|
11
|
+
> no bug fixes, no new Diffbot API features, no security fixes.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
The Diffbot Python library and `db` CLI now live at
|
|
16
|
+
**[diffbot](https://pypi.org/project/diffbot/)**. The import name is unchanged
|
|
17
|
+
(`import diffbot`), so no code changes are needed.
|
|
18
|
+
|
|
19
|
+
This release contains the same code as `diffbot` 3.0.0 so existing installs keep
|
|
20
|
+
working, but it prints a warning on every import until you migrate.
|
|
21
|
+
|
|
22
|
+
## Migrating
|
|
23
|
+
|
|
24
|
+
1. Replace `diffbot-python` with `diffbot` in your `requirements.txt`, `pyproject.toml`, etc.
|
|
25
|
+
2. Uninstall the old package **before** installing the new one:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip uninstall diffbot-python
|
|
29
|
+
pip install diffbot
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Both packages provide the same `diffbot` module, so uninstalling
|
|
33
|
+
`diffbot-python` after `diffbot` is installed deletes files that `diffbot`
|
|
34
|
+
needs. If that happened, run `pip install --force-reinstall diffbot`.
|
|
35
|
+
|
|
36
|
+
Source and issues: <https://github.com/diffbot/diffbot-python>.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Final release under the old distribution name. diffbot-python was renamed to
|
|
2
|
+
# diffbot (3.0.0+). This release freezes the current library code under the old
|
|
3
|
+
# name and warns on import, so `pip install -U diffbot-python` keeps working
|
|
4
|
+
# while pointing users at the new package. It deliberately does NOT depend on
|
|
5
|
+
# diffbot: both ship the same top-level `diffbot` module, and installing them
|
|
6
|
+
# together lets pip delete one's files when it uninstalls the other.
|
|
7
|
+
#
|
|
8
|
+
# `make build-legacy` copies ../../src next to this file before building, so
|
|
9
|
+
# keep dependencies and scripts in step with the main pyproject.toml.
|
|
10
|
+
# Published with `make release-legacy`.
|
|
11
|
+
[build-system]
|
|
12
|
+
requires = ["hatchling"]
|
|
13
|
+
build-backend = "hatchling.build"
|
|
14
|
+
|
|
15
|
+
[project]
|
|
16
|
+
name = "diffbot-python"
|
|
17
|
+
version = "0.3.0"
|
|
18
|
+
description = "⚠️ LEGACY — renamed to diffbot. Final release, no further updates: pip install diffbot"
|
|
19
|
+
readme = "README.md"
|
|
20
|
+
requires-python = ">=3.9"
|
|
21
|
+
license = "MIT"
|
|
22
|
+
license-files = ["LICENSE"]
|
|
23
|
+
authors = [
|
|
24
|
+
{ name = "Jerome Choo", email = "jerome@diffbot.com" },
|
|
25
|
+
{ name = "Mike Tung", email = "miket@diffbot.com" }
|
|
26
|
+
]
|
|
27
|
+
classifiers = [
|
|
28
|
+
"Development Status :: 7 - Inactive",
|
|
29
|
+
"Programming Language :: Python :: 3",
|
|
30
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
31
|
+
]
|
|
32
|
+
dependencies = [
|
|
33
|
+
"httpx>=0.27.0",
|
|
34
|
+
"click>=8.1.0",
|
|
35
|
+
"rich>=13.0.0",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://pypi.org/project/diffbot/"
|
|
40
|
+
Repository = "https://github.com/diffbot/diffbot-python"
|
|
41
|
+
|
|
42
|
+
[project.scripts]
|
|
43
|
+
db = "diffbot.cli:main"
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build.targets.wheel]
|
|
46
|
+
packages = ["src/diffbot"]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
diffbot - Python client library for the Diffbot APIs.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import warnings as _warnings
|
|
6
|
+
from importlib.metadata import PackageNotFoundError, version as _version
|
|
7
|
+
|
|
8
|
+
def _installed_version(dist: str):
|
|
9
|
+
try:
|
|
10
|
+
return _version(dist)
|
|
11
|
+
except PackageNotFoundError:
|
|
12
|
+
return None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# The distribution was renamed from diffbot-python to diffbot. The final
|
|
16
|
+
# diffbot-python release ships this same code (see legacy/diffbot-python), so
|
|
17
|
+
# fall back to its version when that's what is installed.
|
|
18
|
+
_legacy_version = _installed_version("diffbot-python")
|
|
19
|
+
# "0.0.0" when not installed (e.g. running from a source tree).
|
|
20
|
+
__version__ = _installed_version("diffbot") or _legacy_version or "0.0.0"
|
|
21
|
+
|
|
22
|
+
# Both distributions ship this module, so a diffbot-python install (alone, or
|
|
23
|
+
# left over next to diffbot) should migrate. FutureWarning, unlike
|
|
24
|
+
# DeprecationWarning, is shown by default wherever the import happens.
|
|
25
|
+
if _legacy_version is not None:
|
|
26
|
+
_warnings.warn(
|
|
27
|
+
"diffbot-python has been renamed to diffbot and will receive no further "
|
|
28
|
+
"updates. Run `pip uninstall diffbot-python && pip install "
|
|
29
|
+
"--force-reinstall diffbot` and replace diffbot-python with diffbot in "
|
|
30
|
+
"your requirements.",
|
|
31
|
+
FutureWarning,
|
|
32
|
+
stacklevel=2,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
from ._auth import resolve_token
|
|
36
|
+
from .ask import json_schema_format
|
|
37
|
+
from .client import Diffbot, DiffbotAsync
|
|
38
|
+
from .crawl import CrawlEvent, CrawlEventType
|
|
39
|
+
from .errors import (
|
|
40
|
+
APIError,
|
|
41
|
+
AuthError,
|
|
42
|
+
DiffbotError,
|
|
43
|
+
ExtractionError,
|
|
44
|
+
RateLimitError,
|
|
45
|
+
ValidationError,
|
|
46
|
+
)
|
|
47
|
+
from .ontology import Ontology
|
|
48
|
+
|
|
49
|
+
__all__ = [
|
|
50
|
+
"Diffbot",
|
|
51
|
+
"DiffbotAsync",
|
|
52
|
+
"resolve_token",
|
|
53
|
+
"json_schema_format",
|
|
54
|
+
"CrawlEvent",
|
|
55
|
+
"CrawlEventType",
|
|
56
|
+
"Ontology",
|
|
57
|
+
"DiffbotError",
|
|
58
|
+
"AuthError",
|
|
59
|
+
"ExtractionError",
|
|
60
|
+
"RateLimitError",
|
|
61
|
+
"APIError",
|
|
62
|
+
"ValidationError",
|
|
63
|
+
]
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Diffbot LLM RAG API: stream a chat completion."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from typing import TYPE_CHECKING, Any, AsyncIterator, Dict, Iterator, List, Optional
|
|
6
|
+
|
|
7
|
+
from .errors import ValidationError
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from .client import Diffbot, DiffbotAsync
|
|
11
|
+
|
|
12
|
+
MODEL = "diffbot-small-xl"
|
|
13
|
+
|
|
14
|
+
#: `response_format` types accepted by the Diffbot LLM endpoint.
|
|
15
|
+
RESPONSE_FORMAT_TYPES = ("text", "json_object", "json_schema")
|
|
16
|
+
|
|
17
|
+
# The RAG loop may prefix its final answer with a think block, and the model
|
|
18
|
+
# occasionally wraps JSON in a markdown fence despite being told not to.
|
|
19
|
+
_THINK_BLOCK = re.compile(r"<think>.*?</think>", re.DOTALL)
|
|
20
|
+
_JSON_FENCE = re.compile(r"^```(?:json)?\s*|\s*```$", re.MULTILINE)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def json_schema_format(schema: Dict[str, Any], *, name: str = "response") -> Dict[str, Any]:
|
|
24
|
+
"""Build a ``response_format`` value that constrains output to ``schema``.
|
|
25
|
+
|
|
26
|
+
The endpoint requires the schema nested under ``json_schema.schema``; passing
|
|
27
|
+
it anywhere else is ignored server-side without an error, so prefer this
|
|
28
|
+
helper over hand-building the dict.
|
|
29
|
+
|
|
30
|
+
Example:
|
|
31
|
+
>>> json_schema_format({"type": "object", "properties": {"city": {"type": "string"}}})
|
|
32
|
+
{'type': 'json_schema', 'json_schema': {'name': 'response', 'schema': {...}}}
|
|
33
|
+
"""
|
|
34
|
+
if not isinstance(schema, dict):
|
|
35
|
+
raise ValidationError("schema must be a JSON Schema dict")
|
|
36
|
+
return {"type": "json_schema", "json_schema": {"name": name, "schema": schema}}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _validate_response_format(response_format: Optional[Dict[str, Any]]) -> None:
|
|
40
|
+
"""Reject shapes the endpoint would accept but silently not enforce."""
|
|
41
|
+
if response_format is None:
|
|
42
|
+
return
|
|
43
|
+
if not isinstance(response_format, dict):
|
|
44
|
+
raise ValidationError("response_format must be a dict")
|
|
45
|
+
|
|
46
|
+
fmt_type = response_format.get("type", "text")
|
|
47
|
+
if fmt_type not in RESPONSE_FORMAT_TYPES:
|
|
48
|
+
raise ValidationError(
|
|
49
|
+
f"response_format type must be one of {', '.join(RESPONSE_FORMAT_TYPES)}; got {fmt_type!r}"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if fmt_type == "json_schema":
|
|
53
|
+
json_schema = response_format.get("json_schema")
|
|
54
|
+
if not isinstance(json_schema, dict) or "schema" not in json_schema:
|
|
55
|
+
raise ValidationError(
|
|
56
|
+
'response_format {"type": "json_schema"} requires the schema nested as '
|
|
57
|
+
'{"json_schema": {"schema": {...}}}. Without it the server returns 200 and '
|
|
58
|
+
"ignores the constraint. Use diffbot.json_schema_format(schema) to build it."
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _build_payload(
|
|
63
|
+
client: Any,
|
|
64
|
+
messages: List[Dict[str, str]],
|
|
65
|
+
*,
|
|
66
|
+
response_format: Optional[Dict[str, Any]] = None,
|
|
67
|
+
) -> tuple:
|
|
68
|
+
_validate_response_format(response_format)
|
|
69
|
+
headers = {"Authorization": f"Bearer {client.token}"}
|
|
70
|
+
payload: Dict[str, Any] = {"model": MODEL, "messages": messages, "stream": True}
|
|
71
|
+
if response_format is not None:
|
|
72
|
+
payload["response_format"] = response_format
|
|
73
|
+
return headers, payload
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _parse_chunk(line: str):
|
|
77
|
+
try:
|
|
78
|
+
chunk = json.loads(line.replace("data: ", ""))
|
|
79
|
+
except json.JSONDecodeError:
|
|
80
|
+
return None
|
|
81
|
+
choices = chunk.get("choices")
|
|
82
|
+
if choices and choices[0].get("delta", {}).get("content"):
|
|
83
|
+
return choices[0]["delta"]["content"]
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _extract_json(text: str) -> Any:
|
|
88
|
+
"""Parse the model's final answer as JSON, tolerating think blocks and fences."""
|
|
89
|
+
cleaned = _THINK_BLOCK.sub("", text).strip()
|
|
90
|
+
cleaned = _JSON_FENCE.sub("", cleaned).strip()
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
return json.loads(cleaned)
|
|
94
|
+
except json.JSONDecodeError:
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
# Fall back to the outermost object or array span in the response.
|
|
98
|
+
spans = []
|
|
99
|
+
for opener, closer in (("{", "}"), ("[", "]")):
|
|
100
|
+
start, end = cleaned.find(opener), cleaned.rfind(closer)
|
|
101
|
+
if start != -1 and end > start:
|
|
102
|
+
spans.append((start, cleaned[start : end + 1]))
|
|
103
|
+
for _, span in sorted(spans):
|
|
104
|
+
try:
|
|
105
|
+
return json.loads(span)
|
|
106
|
+
except json.JSONDecodeError:
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
raise ValidationError(f"could not parse JSON from the model response: {text[:200]!r}")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
#: Schema used when the caller wants JSON but has no shape in mind. This goes
|
|
113
|
+
#: through the json_schema path rather than {"type": "json_object"} on purpose:
|
|
114
|
+
#: json_object applies no server-side grammar, so the RAG loop's internal
|
|
115
|
+
#: <functioncall> JSON satisfies it and gets returned as the final answer. That
|
|
116
|
+
#: is reproducible whenever the request carries a system message.
|
|
117
|
+
ANY_OBJECT_SCHEMA = {"type": "object"}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _resolve_format(
|
|
121
|
+
schema: Optional[Dict[str, Any]],
|
|
122
|
+
response_format: Optional[Dict[str, Any]],
|
|
123
|
+
) -> Dict[str, Any]:
|
|
124
|
+
if schema is not None and response_format is not None:
|
|
125
|
+
raise ValidationError("pass either schema or response_format, not both")
|
|
126
|
+
if response_format is not None:
|
|
127
|
+
return response_format
|
|
128
|
+
return json_schema_format(schema if schema is not None else ANY_OBJECT_SCHEMA)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _check_tool_call_leak(parsed: Any) -> Any:
|
|
132
|
+
"""Catch the internal tool call surfacing as the answer (see ANY_OBJECT_SCHEMA)."""
|
|
133
|
+
if isinstance(parsed, dict) and parsed.get("name") == "functioncall" and "arguments" in parsed:
|
|
134
|
+
raise ValidationError(
|
|
135
|
+
"the model returned its internal tool call instead of an answer; this happens with "
|
|
136
|
+
'response_format {"type": "json_object"} because the server applies no grammar to it. '
|
|
137
|
+
"Pass a schema instead."
|
|
138
|
+
)
|
|
139
|
+
return parsed
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def ask(
|
|
143
|
+
client: "Diffbot",
|
|
144
|
+
messages: List[Dict[str, str]],
|
|
145
|
+
*,
|
|
146
|
+
response_format: Optional[Dict[str, Any]] = None,
|
|
147
|
+
) -> Iterator[str]:
|
|
148
|
+
headers, payload = _build_payload(client, messages, response_format=response_format)
|
|
149
|
+
with client._http.stream("POST", client.llm_url, headers=headers, json=payload) as response:
|
|
150
|
+
client._raise_for_status(response)
|
|
151
|
+
for line in response.iter_lines():
|
|
152
|
+
if line:
|
|
153
|
+
content = _parse_chunk(line)
|
|
154
|
+
if content:
|
|
155
|
+
yield content
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
async def ask_async(
|
|
159
|
+
client: "DiffbotAsync",
|
|
160
|
+
messages: List[Dict[str, str]],
|
|
161
|
+
*,
|
|
162
|
+
response_format: Optional[Dict[str, Any]] = None,
|
|
163
|
+
) -> AsyncIterator[str]:
|
|
164
|
+
headers, payload = _build_payload(client, messages, response_format=response_format)
|
|
165
|
+
async with client._http.stream("POST", client.llm_url, headers=headers, json=payload) as response:
|
|
166
|
+
client._raise_for_status(response)
|
|
167
|
+
async for line in response.aiter_lines():
|
|
168
|
+
if line:
|
|
169
|
+
content = _parse_chunk(line)
|
|
170
|
+
if content:
|
|
171
|
+
yield content
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def ask_json(
|
|
175
|
+
client: "Diffbot",
|
|
176
|
+
messages: List[Dict[str, str]],
|
|
177
|
+
schema: Optional[Dict[str, Any]] = None,
|
|
178
|
+
*,
|
|
179
|
+
response_format: Optional[Dict[str, Any]] = None,
|
|
180
|
+
) -> Any:
|
|
181
|
+
fmt = _resolve_format(schema, response_format)
|
|
182
|
+
text = "".join(ask(client, messages, response_format=fmt))
|
|
183
|
+
return _check_tool_call_leak(_extract_json(text))
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
async def ask_json_async(
|
|
187
|
+
client: "DiffbotAsync",
|
|
188
|
+
messages: List[Dict[str, str]],
|
|
189
|
+
schema: Optional[Dict[str, Any]] = None,
|
|
190
|
+
*,
|
|
191
|
+
response_format: Optional[Dict[str, Any]] = None,
|
|
192
|
+
) -> Any:
|
|
193
|
+
fmt = _resolve_format(schema, response_format)
|
|
194
|
+
chunks = [chunk async for chunk in ask_async(client, messages, response_format=fmt)]
|
|
195
|
+
return _check_tool_call_leak(_extract_json("".join(chunks)))
|
|
@@ -11,7 +11,15 @@ from rich.markdown import Markdown
|
|
|
11
11
|
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
12
12
|
from datetime import datetime
|
|
13
13
|
|
|
14
|
-
from diffbot import
|
|
14
|
+
from diffbot import (
|
|
15
|
+
__version__,
|
|
16
|
+
CrawlEventType,
|
|
17
|
+
AuthError,
|
|
18
|
+
ExtractionError,
|
|
19
|
+
APIError,
|
|
20
|
+
ValidationError,
|
|
21
|
+
json_schema_format,
|
|
22
|
+
)
|
|
15
23
|
|
|
16
24
|
from ._common import get_client
|
|
17
25
|
|
|
@@ -86,10 +94,29 @@ def extract(url, output, fmt, api):
|
|
|
86
94
|
@click.argument("prompt")
|
|
87
95
|
@click.option("-o", "--output", type=click.Path(), help="Write output to file instead of stdout")
|
|
88
96
|
@click.option("--json", "as_json", is_flag=True, help="Output from LLM as JSON")
|
|
89
|
-
|
|
97
|
+
@click.option(
|
|
98
|
+
"--schema",
|
|
99
|
+
"schema_path",
|
|
100
|
+
type=click.Path(exists=True, dir_okay=False),
|
|
101
|
+
help="JSON Schema file constraining the output (implies --json)",
|
|
102
|
+
)
|
|
103
|
+
def ask(prompt: str, output: str = None, as_json: bool = False, schema_path: str = None):
|
|
90
104
|
"""Ask a question to the Diffbot LLM"""
|
|
91
105
|
db = get_client()
|
|
92
106
|
stdin_content = sys.stdin.read() if not sys.stdin.isatty() else None
|
|
107
|
+
|
|
108
|
+
response_format = None
|
|
109
|
+
if schema_path:
|
|
110
|
+
as_json = True
|
|
111
|
+
try:
|
|
112
|
+
with open(schema_path) as f:
|
|
113
|
+
response_format = json_schema_format(json.load(f))
|
|
114
|
+
except json.JSONDecodeError as e:
|
|
115
|
+
click.echo(f"Error: {schema_path} is not valid JSON: {e}", err=True)
|
|
116
|
+
raise click.Abort()
|
|
117
|
+
|
|
118
|
+
# Without --schema, response_format stays None and ask_json picks its own
|
|
119
|
+
# permissive default; --json alone must not become {"type": "json_object"}.
|
|
93
120
|
interactive_mode = is_interactive and not output and not as_json
|
|
94
121
|
|
|
95
122
|
messages = [
|
|
@@ -100,9 +127,6 @@ def ask(prompt: str, output: str = None, as_json: bool = False):
|
|
|
100
127
|
{"role": "user", "content": prompt},
|
|
101
128
|
]
|
|
102
129
|
|
|
103
|
-
if as_json:
|
|
104
|
-
messages[1]["content"] += "\nReturn the output as a JSON object. Do not include any other text outside of the JSON object."
|
|
105
|
-
|
|
106
130
|
if stdin_content:
|
|
107
131
|
messages[1]["content"] = f"<input>{stdin_content}</input>\n" + messages[1]["content"]
|
|
108
132
|
|
|
@@ -137,17 +161,20 @@ def ask(prompt: str, output: str = None, as_json: bool = False):
|
|
|
137
161
|
else:
|
|
138
162
|
response = ""
|
|
139
163
|
if output:
|
|
140
|
-
|
|
141
|
-
response
|
|
164
|
+
if as_json:
|
|
165
|
+
response = json.dumps(
|
|
166
|
+
db.ask_json(messages, response_format=response_format), indent=2
|
|
167
|
+
)
|
|
168
|
+
else:
|
|
169
|
+
for chunk in db.ask(messages):
|
|
170
|
+
response += chunk
|
|
142
171
|
with open(output, "w") as f:
|
|
143
172
|
f.write(response)
|
|
144
173
|
click.echo(f"Output written to {output}")
|
|
145
174
|
elif as_json:
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
buffer = buffer[buffer.find("{") : buffer.rfind("}") + 1]
|
|
150
|
-
sys.stdout.write(buffer)
|
|
175
|
+
sys.stdout.write(
|
|
176
|
+
json.dumps(db.ask_json(messages, response_format=response_format), indent=2)
|
|
177
|
+
)
|
|
151
178
|
else:
|
|
152
179
|
for chunk in db.ask(messages):
|
|
153
180
|
sys.stdout.write(chunk)
|
|
@@ -156,6 +183,9 @@ def ask(prompt: str, output: str = None, as_json: bool = False):
|
|
|
156
183
|
except AuthError:
|
|
157
184
|
click.echo("Error: Invalid or unauthorized API token.", err=True)
|
|
158
185
|
raise click.Abort()
|
|
186
|
+
except ValidationError as e:
|
|
187
|
+
click.echo(f"Error: {e}", err=True)
|
|
188
|
+
raise click.Abort()
|
|
159
189
|
except APIError as e:
|
|
160
190
|
click.echo(f"API error {e.status_code}: {e.message or e.body}", err=True)
|
|
161
191
|
raise click.Abort()
|
|
@@ -9,7 +9,12 @@ import httpx
|
|
|
9
9
|
from . import __version__
|
|
10
10
|
from .errors import APIError, AuthError, RateLimitError, ValidationError
|
|
11
11
|
from .extract import extract as _extract, extract_async as _extract_async
|
|
12
|
-
from .ask import
|
|
12
|
+
from .ask import (
|
|
13
|
+
ask as _ask,
|
|
14
|
+
ask_async as _ask_async,
|
|
15
|
+
ask_json as _ask_json,
|
|
16
|
+
ask_json_async as _ask_json_async,
|
|
17
|
+
)
|
|
13
18
|
from .crawl import (
|
|
14
19
|
CrawlEvent,
|
|
15
20
|
crawl as _crawl,
|
|
@@ -22,6 +27,8 @@ from .crawl import (
|
|
|
22
27
|
crawl_list_jobs_async as _crawl_list_jobs_async,
|
|
23
28
|
)
|
|
24
29
|
from .kg import (
|
|
30
|
+
KG_DQL_ENDPOINT,
|
|
31
|
+
KG_ONTOLOGY_ENDPOINT,
|
|
25
32
|
dql as _dql,
|
|
26
33
|
dql_async as _dql_async,
|
|
27
34
|
dql_fetch_ontology as _dql_fetch_ontology,
|
|
@@ -67,6 +74,8 @@ class Diffbot:
|
|
|
67
74
|
crawler_url: str = CRAWL_BASE,
|
|
68
75
|
web_search_url: str = WEB_SEARCH_BASE,
|
|
69
76
|
nlp_url: str = NLP_BASE,
|
|
77
|
+
dql_url: str = KG_DQL_ENDPOINT,
|
|
78
|
+
ontology_url: str = KG_ONTOLOGY_ENDPOINT,
|
|
70
79
|
transport: Optional[httpx.BaseTransport] = None,
|
|
71
80
|
):
|
|
72
81
|
if not token:
|
|
@@ -77,6 +86,8 @@ class Diffbot:
|
|
|
77
86
|
self.crawler_url = crawler_url
|
|
78
87
|
self.web_search_url = web_search_url
|
|
79
88
|
self.nlp_url = nlp_url
|
|
89
|
+
self.dql_url = dql_url
|
|
90
|
+
self.ontology_url = ontology_url
|
|
80
91
|
self._http = httpx.Client(
|
|
81
92
|
timeout=timeout,
|
|
82
93
|
headers={"User-Agent": f"diffbot-python/{__version__}"},
|
|
@@ -112,9 +123,32 @@ class Diffbot:
|
|
|
112
123
|
"""Extract structured content from a URL. Returns the raw Diffbot API response."""
|
|
113
124
|
return _extract(self, url, api=api, fmt=fmt)
|
|
114
125
|
|
|
115
|
-
def ask(
|
|
116
|
-
|
|
117
|
-
|
|
126
|
+
def ask(
|
|
127
|
+
self,
|
|
128
|
+
messages: List[Dict[str, str]],
|
|
129
|
+
*,
|
|
130
|
+
response_format: Optional[Dict[str, Any]] = None,
|
|
131
|
+
) -> Iterator[str]:
|
|
132
|
+
"""Stream a response from the Diffbot LLM RAG API.
|
|
133
|
+
|
|
134
|
+
Pass ``response_format`` to constrain the output, e.g.
|
|
135
|
+
``{"type": "json_object"}`` or ``diffbot.json_schema_format(schema)``.
|
|
136
|
+
"""
|
|
137
|
+
yield from _ask(self, messages, response_format=response_format)
|
|
138
|
+
|
|
139
|
+
def ask_json(
|
|
140
|
+
self,
|
|
141
|
+
messages: List[Dict[str, str]],
|
|
142
|
+
schema: Optional[Dict[str, Any]] = None,
|
|
143
|
+
*,
|
|
144
|
+
response_format: Optional[Dict[str, Any]] = None,
|
|
145
|
+
) -> Any:
|
|
146
|
+
"""Ask the Diffbot LLM and return the answer parsed as JSON.
|
|
147
|
+
|
|
148
|
+
With ``schema`` (a JSON Schema dict) the model is constrained to it by
|
|
149
|
+
grammar; without one it falls back to unconstrained JSON output.
|
|
150
|
+
"""
|
|
151
|
+
return _ask_json(self, messages, schema, response_format=response_format)
|
|
118
152
|
|
|
119
153
|
def crawl(self, site: str, **kwargs: Any) -> Iterator[CrawlEvent]:
|
|
120
154
|
"""Start a crawl job."""
|
|
@@ -194,6 +228,8 @@ class DiffbotAsync:
|
|
|
194
228
|
crawler_url: str = CRAWL_BASE,
|
|
195
229
|
web_search_url: str = WEB_SEARCH_BASE,
|
|
196
230
|
nlp_url: str = NLP_BASE,
|
|
231
|
+
dql_url: str = KG_DQL_ENDPOINT,
|
|
232
|
+
ontology_url: str = KG_ONTOLOGY_ENDPOINT,
|
|
197
233
|
transport: Optional[httpx.AsyncBaseTransport] = None,
|
|
198
234
|
):
|
|
199
235
|
if not token:
|
|
@@ -204,6 +240,8 @@ class DiffbotAsync:
|
|
|
204
240
|
self.crawler_url = crawler_url
|
|
205
241
|
self.web_search_url = web_search_url
|
|
206
242
|
self.nlp_url = nlp_url
|
|
243
|
+
self.dql_url = dql_url
|
|
244
|
+
self.ontology_url = ontology_url
|
|
207
245
|
self._http = httpx.AsyncClient(
|
|
208
246
|
timeout=timeout,
|
|
209
247
|
headers={"User-Agent": f"diffbot-python/{__version__}"},
|
|
@@ -239,11 +277,34 @@ class DiffbotAsync:
|
|
|
239
277
|
"""Extract structured content from a URL. Returns the raw Diffbot API response."""
|
|
240
278
|
return await _extract_async(self, url, api=api, fmt=fmt)
|
|
241
279
|
|
|
242
|
-
async def ask(
|
|
243
|
-
|
|
244
|
-
|
|
280
|
+
async def ask(
|
|
281
|
+
self,
|
|
282
|
+
messages: List[Dict[str, str]],
|
|
283
|
+
*,
|
|
284
|
+
response_format: Optional[Dict[str, Any]] = None,
|
|
285
|
+
) -> AsyncIterator[str]:
|
|
286
|
+
"""Stream a response from the Diffbot LLM RAG API.
|
|
287
|
+
|
|
288
|
+
Pass ``response_format`` to constrain the output, e.g.
|
|
289
|
+
``{"type": "json_object"}`` or ``diffbot.json_schema_format(schema)``.
|
|
290
|
+
"""
|
|
291
|
+
async for chunk in _ask_async(self, messages, response_format=response_format):
|
|
245
292
|
yield chunk
|
|
246
293
|
|
|
294
|
+
async def ask_json(
|
|
295
|
+
self,
|
|
296
|
+
messages: List[Dict[str, str]],
|
|
297
|
+
schema: Optional[Dict[str, Any]] = None,
|
|
298
|
+
*,
|
|
299
|
+
response_format: Optional[Dict[str, Any]] = None,
|
|
300
|
+
) -> Any:
|
|
301
|
+
"""Ask the Diffbot LLM and return the answer parsed as JSON.
|
|
302
|
+
|
|
303
|
+
With ``schema`` (a JSON Schema dict) the model is constrained to it by
|
|
304
|
+
grammar; without one it falls back to unconstrained JSON output.
|
|
305
|
+
"""
|
|
306
|
+
return await _ask_json_async(self, messages, schema, response_format=response_format)
|
|
307
|
+
|
|
247
308
|
async def crawl(self, site: str, **kwargs: Any) -> AsyncIterator[CrawlEvent]:
|
|
248
309
|
"""Start a crawl job. Pass watch=True to poll until completion and yield URL_PROCESSED events."""
|
|
249
310
|
async for event in _crawl_async(self, site, **kwargs):
|
|
@@ -51,7 +51,7 @@ def dql(
|
|
|
51
51
|
raw: bool = False,
|
|
52
52
|
) -> Union[Dict[str, Any], bytes]:
|
|
53
53
|
params = _build_dql_params(client, query, size, from_, format, filter, exportspec, extra)
|
|
54
|
-
response = client._http.get(
|
|
54
|
+
response = client._http.get(client.dql_url, params=params)
|
|
55
55
|
client._raise_for_status(response)
|
|
56
56
|
return response.content if raw else response.json()
|
|
57
57
|
|
|
@@ -69,7 +69,7 @@ async def dql_async(
|
|
|
69
69
|
raw: bool = False,
|
|
70
70
|
) -> Union[Dict[str, Any], bytes]:
|
|
71
71
|
params = _build_dql_params(client, query, size, from_, format, filter, exportspec, extra)
|
|
72
|
-
response = await client._http.get(
|
|
72
|
+
response = await client._http.get(client.dql_url, params=params)
|
|
73
73
|
client._raise_for_status(response)
|
|
74
74
|
return response.content if raw else response.json()
|
|
75
75
|
|
|
@@ -104,7 +104,7 @@ async def dql_parallel_async(
|
|
|
104
104
|
|
|
105
105
|
|
|
106
106
|
def dql_refresh_ontology(client: "Diffbot", dest: pathlib.Path) -> None:
|
|
107
|
-
response = client._http.get(
|
|
107
|
+
response = client._http.get(client.ontology_url)
|
|
108
108
|
client._raise_for_status(response)
|
|
109
109
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
110
110
|
dest.write_bytes(response.content)
|
|
@@ -116,13 +116,13 @@ def dql_fetch_ontology(client: "Diffbot") -> Ontology:
|
|
|
116
116
|
Performs no caching — the caller decides whether and where to hold onto the
|
|
117
117
|
result. Use :func:`dql_refresh_ontology` instead to persist raw bytes to disk.
|
|
118
118
|
"""
|
|
119
|
-
response = client._http.get(
|
|
119
|
+
response = client._http.get(client.ontology_url)
|
|
120
120
|
client._raise_for_status(response)
|
|
121
121
|
return Ontology.from_json(response.content)
|
|
122
122
|
|
|
123
123
|
|
|
124
124
|
async def dql_fetch_ontology_async(client: "DiffbotAsync") -> Ontology:
|
|
125
125
|
"""Async variant of :func:`dql_fetch_ontology`."""
|
|
126
|
-
response = await client._http.get(
|
|
126
|
+
response = await client._http.get(client.ontology_url)
|
|
127
127
|
client._raise_for_status(response)
|
|
128
128
|
return Ontology.from_json(response.content)
|