返回提交历史
Modified
docs/pydantic_ai.md
+67
-0
Modified
g4f/Provider/PollinationsAI.py
+8
-6
Modified
g4f/Provider/hf/HuggingFaceAPI.py
+2
-2
Modified
g4f/Provider/hf/models.py
+1
-0
Modified
g4f/Provider/template/OpenaiTemplate.py
+0
-1
Modified
g4f/client/__init__.py
+6
-4
Modified
g4f/debug.py
+4
-4
Modified
g4f/gui/client/demo.html
+7
-3
Added
g4f/integration/__init__.py
+0
-0
Added
g4f/providers/tool_support.py
+77
-0
Deleted
g4f/tools/pydantic_ai.py
+0
-1
XFEstudio/gpt4free
Add ToolSupportProvider
989f02fc
代码差异
11 个文件
+172
-21
@@ -129,6 +129,73 @@ This example demonstrates the use of a custom Pydantic model (`MyModel`) to capt
129
129
130
130
---
131
131
132
### Support for Models/Providers without Tool Call Suport
133
134
For models/providers that do not fully support tool calls or lack a direct API for structured output, the `ToolSupportProvider` can be used to bridge the gap. This provider ensures that the agent properly formats the response, even when the model itself doesn't have built-in support for structured outputs. It does so by leveraging a tool list and creating a response format when only one tool is used.
135
136
### Example for Models/Providers without Tool Support (Single Tool Usage)
137
138
```python
139
from pydantic import BaseModel
140
from pydantic_ai import Agent
141
from pydantic_ai.models import ModelSettings
142
from g4f.integration.pydantic_ai import AIModel
143
from g4f.providers.tool_support import ToolSupportProvider
144
145
from g4f import debug
146
debug.logging = True
147
148
# Define a custom model for structured output (e.g., city and country)
149
class MyModel(BaseModel):
150
city: str
151
country: str
152
153
# Create the agent for a model with tool support (using one tool)
154
agent = Agent(AIModel(
155
"PollinationsAI:gpt-4o", # Specify the provider and model
156
ToolSupportProvider # Use ToolSupportProvider to handle tool-based response formatting
157
), result_type=MyModel, model_settings=ModelSettings(temperature=0))
158
159
if __name__ == '__main__':
160
# Run the agent with a query to extract information (e.g., city and country)
161
result = agent.run_sync('European city with the bear.')
162
print(result.data) # Structured output of city and country
163
print(result.usage()) # Usage statistics
164
```
165
166
### Explanation:
167
168
- **`ToolSupportProvider` as a Bridge:** The `ToolSupportProvider` acts as a bridge between the agent and the model, ensuring that the response is formatted into a structured output, even if the model doesn't have an API that directly supports such formatting.
169
170
- For instance, if the model generates raw text or unstructured data, the `ToolSupportProvider` will convert this into the expected format (like `MyModel`), allowing the agent to process it as structured data.
171
172
- **Model Initialization:** We initialize the agent with the `PollinationsAI:gpt-4o` model, which may not have a built-in API for returning structured outputs. Instead, it relies on the `ToolSupportProvider` to format the output.
173
174
- **Custom Result Model:** We define a custom Pydantic model (`MyModel`) to capture the expected output in a structured way (e.g., `city` and `country` fields). This helps ensure that even when the model doesn't support structured data, the agent can interpret and format it.
175
176
- **Debug Logging:** The `g4f.debug.logging` is enabled to provide detailed logs for troubleshooting and monitoring the agent's execution.
177
178
### Example Output:
179
180
```bash
181
city='Berlin'
182
country='Germany'
183
usage={'prompt_tokens': 15, 'completion_tokens': 50}
184
```
185
186
### Key Points:
187
188
- **`ToolSupportProvider` Role:** The `ToolSupportProvider` ensures that the agent formats the raw or unstructured response from the model into a structured format, even if the model itself lacks built-in support for structured data.
189
190
- **Single Tool Usage:** The `ToolSupportProvider` is particularly useful when only one tool is used by the model, and it needs to format or transform the model's output into a structured response without additional tools.
191
192
### Notes:
193
194
- This approach is ideal for models that return unstructured text or data that needs to be transformed into a structured format (e.g., Pydantic models).
195
- The `ToolSupportProvider` bridges the gap between the model's output and the expected structured format, enabling seamless integration into workflows that require structured responses.
196
197
---
198
132
199
## LangChain Integration Example
133
200
134
201
For users working with LangChain, here is an example demonstrating how to integrate G4F models into a LangChain environment:
@@ -1,6 +1,5 @@
1
1
from __future__ import annotations
2
2
3
import json
4
3
import random
5
4
import requests
6
5
from urllib.parse import quote_plus
@@ -15,6 +14,7 @@ from ..errors import ModelNotFoundError
15
14
from ..requests.raise_for_status import raise_for_status
16
15
from ..requests.aiohttp import get_connector
17
16
from ..providers.response import ImageResponse, ImagePreview, FinishReason, Usage
17
from .. import debug
18
18
19
19
DEFAULT_HEADERS = {
20
20
'Accept': '*/*',
@@ -74,9 +74,11 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
74
74
try:
75
75
# Update of image models
76
76
image_response = requests.get("https://image.pollinations.ai/models")
77
image_response.raise_for_status()
78
new_image_models = image_response.json()
79
77
if image_response.ok:
78
new_image_models = image_response.json()
79
else:
80
new_image_models = []
81
80
82
# Combine models without duplicates
81
83
all_image_models = (
82
84
cls.image_models + # Already contains the default
@@ -112,8 +114,8 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
112
114
cls.text_models = [cls.default_model]
113
115
if not cls.image_models:
114
116
cls.image_models = [cls.default_image_model]
115
raise RuntimeError(f"Failed to fetch models: {e}") from e
116
117
debug.error(f"Failed to fetch models: {e}")
118
117
119
return cls.text_models + cls.image_models
118
120
119
121
@classmethod
@@ -61,10 +61,10 @@ class HuggingFaceAPI(OpenaiTemplate):
61
61
images: ImagesType = None,
62
62
**kwargs
63
63
):
64
if model in cls.model_aliases:
65
model = cls.model_aliases[model]
66
64
if model == llama_models["name"]:
67
65
model = llama_models["text"] if images is None else llama_models["vision"]
66
if model in cls.model_aliases:
67
model = cls.model_aliases[model]
68
68
api_base = f"https://api-inference.huggingface.co/models/{model}/v1"
69
69
pipeline_tag = await cls.get_pipline_tag(model, api_key)
70
70
if pipeline_tag not in ("text-generation", "image-text-to-text"):
@@ -20,6 +20,7 @@ fallback_models = text_models + image_models
20
20
model_aliases = {
21
21
### Chat ###
22
22
"qwen-2.5-72b": "Qwen/Qwen2.5-Coder-32B-Instruct",
23
"llama-3": "meta-llama/Llama-3.3-70B-Instruct",
23
24
"llama-3.3-70b": "meta-llama/Llama-3.3-70B-Instruct",
24
25
"command-r-plus": "CohereForAI/c4ai-command-r-plus-08-2024",
25
26
"deepseek-r1": "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B",
@@ -145,7 +145,6 @@ class OpenaiTemplate(AsyncGeneratorProvider, ProviderModelMixin, RaiseErrorMixin
145
145
elif content_type.startswith("text/event-stream"):
146
146
await raise_for_status(response)
147
147
first = True
148
is_thinking = 0
149
148
async for line in response.iter_lines():
150
149
if line.startswith(b"data: "):
151
150
chunk = line[6:]
@@ -275,6 +275,7 @@ class Completions:
275
275
276
276
def create(
277
277
self,
278
*,
278
279
messages: Messages,
279
280
model: str,
280
281
provider: Optional[ProviderType] = None,
@@ -306,8 +307,8 @@ class Completions:
306
307
307
308
response = iter_run_tools(
308
309
provider.get_create_function(),
309
model,
310
messages,
310
model=model,
311
messages=messages,
311
312
stream=stream,
312
313
**filter_none(
313
314
proxy=self.client.proxy if proxy is None else proxy,
@@ -561,6 +562,7 @@ class AsyncCompletions:
561
562
562
563
def create(
563
564
self,
565
*,
564
566
messages: Messages,
565
567
model: str,
566
568
provider: Optional[ProviderType] = None,
@@ -592,8 +594,8 @@ class AsyncCompletions:
592
594
593
595
response = async_iter_run_tools(
594
596
provider,
595
model,
596
messages,
597
model=model,
598
messages=messages,
597
599
stream=stream,
598
600
**filter_none(
599
601
proxy=self.client.proxy if proxy is None else proxy,
@@ -1,10 +1,7 @@
1
1
import sys
2
from .providers.types import ProviderType
3
2
4
3
logging: bool = False
5
4
version_check: bool = True
6
last_provider: ProviderType = None
7
last_model: str = None
8
5
version: str = None
9
6
log_handler: callable = print
10
7
logs: list = []
@@ -14,4 +11,7 @@ def log(text, file = None):
14
11
log_handler(text, file=file)
15
12
16
13
def error(error, name: str = None):
17
log(error if isinstance(error, str) else f"{type(error).__name__ if name is None else name}: {error}", file=sys.stderr)
14
log(
15
error if isinstance(error, str) else f"{type(error).__name__ if name is None else name}: {error}",
16
file=sys.stderr
17
)
@@ -201,7 +201,7 @@
201
201
</head>
202
202
<body>
203
203
<iframe id="background"></iframe>
204
<img id="image-feed" alt="Image Feed">
204
<img id="image-feed" class="hidden" alt="Image Feed">
205
205
206
206
<!-- Gradient Background Circle -->
207
207
<div class="gradient"></div>
@@ -336,14 +336,15 @@
336
336
const images = []
337
337
eventSource.onmessage = (event) => {
338
338
const data = JSON.parse(event.data);
339
if (data.nsfw || !data.nologo || data.width < 1024 || !data.imageURL) {
339
if (data.nsfw || !data.nologo || data.width < 1024 || !data.imageURL || data.isChild) {
340
340
return;
341
341
}
342
342
const lower = data.prompt.toLowerCase();
343
const tags = ["logo", "infographic", "warts","prostitute", "curvy", "breasts", "written", "bodies", "naked", "classroom", "malone", "dirty", "shoes", "shower", "banner", "fat", "nipples", "couple", "sexual", "sandal", "supplier", "overlord", "succubus", "platinum", "cracy", "crazy", "lamic", "ropes", "cables", "wires", "dirty", "messy", "cluttered", "chaotic", "disorganized", "disorderly", "untidy", "unorganized", "unorderly", "unsystematic", "disarranged", "disarrayed", "disheveled", "disordered", "jumbled", "muddled", "scattered", "shambolic", "sloppy", "unkept", "unruly"];
343
const tags = ["nsfw", "timeline", "soap", "orally", "heel", "latex", "bathroom", "boobs", "charts", " text ", "gel", "logo", "infographic", "warts", " bra ", "prostitute", "curvy", "breasts", "written", "bodies", "naked", "classroom", "malone", "dirty", "shoes", "shower", "banner", "fat", "nipples", "couple", "sexual", "sandal", "supplier", "overlord", "succubus", "platinum", "cracy", "crazy", "lamic", "ropes", "cables", "wires", "dirty", "messy", "cluttered", "chaotic", "disorganized", "disorderly", "untidy", "unorganized", "unorderly", "unsystematic", "disarranged", "disarrayed", "disheveled", "disordered", "jumbled", "muddled", "scattered", "shambolic", "sloppy", "unkept", "unruly"];
344
344
for (i in tags) {
345
345
if (lower.indexOf(tags[i]) != -1) {
346
346
console.log("Skipping image with tag: " + tags[i]);
347
console.debug("Skipping image:", data.imageURL);
347
348
return;
348
349
}
349
350
}
@@ -354,7 +355,10 @@
354
355
}
355
356
setInterval(() => {
356
357
if (images.length > 0) {
358
imageFeed.classList.remove("hidden");
357
359
imageFeed.src = images.shift();
360
} else if(imageFeed) {
361
imageFeed.remove();
358
362
}
359
363
}, 7000);
360
364
})();
此文件没有可显示的逐行差异。
@@ -0,0 +1,77 @@
1
from __future__ import annotations
2
3
import json
4
5
from ..typing import AsyncResult, Messages, ImagesType
6
from ..providers.asyncio import to_async_iterator
7
from ..client.service import get_model_and_provider
8
from ..client.helper import filter_json
9
from .base_provider import AsyncGeneratorProvider
10
from .response import ToolCalls, FinishReason
11
12
class ToolSupportProvider(AsyncGeneratorProvider):
13
working = True
14
15
@classmethod
16
async def create_async_generator(
17
cls,
18
model: str,
19
messages: Messages,
20
stream: bool = True,
21
images: ImagesType = None,
22
tools: list[str] = None,
23
response_format: dict = None,
24
**kwargs
25
) -> AsyncResult:
26
provider = None
27
if ":" in model:
28
provider, model = model.split(":", 1)
29
model, provider = get_model_and_provider(
30
model, provider,
31
stream, logging=False,
32
has_images=images is not None
33
)
34
if response_format is None:
35
response_format = {"type": "json"}
36
37
if tools is not None:
38
if len(tools) > 1:
39
raise ValueError("Only one tool is supported.")
40
tools = tools.pop()
41
lines = ["Respone in JSON format."]
42
properties = tools["function"]["parameters"]["properties"]
43
properties = {key: value["type"] for key, value in properties.items()}
44
lines.append(f"Response format: {json.dumps(properties, indent=2)}")
45
messages = [{"role": "user", "content": "\n".join(lines)}] + messages
46
47
finish = None
48
chunks = []
49
async for chunk in provider.get_async_create_function()(
50
model,
51
messages,
52
stream=stream,
53
images=images,
54
response_format=response_format,
55
**kwargs
56
):
57
if isinstance(chunk, FinishReason):
58
finish = chunk
59
break
60
elif isinstance(chunk, str):
61
chunks.append(chunk)
62
else:
63
yield chunk
64
65
chunks = "".join(chunks)
66
if tools is not None:
67
yield ToolCalls([{
68
"id": "",
69
"type": "function",
70
"function": {
71
"name": tools["function"]["name"],
72
"arguments": filter_json(chunks)
73
}
74
}])
75
yield chunks
76
if finish is not None:
77
yield finish
@@ -1 +0,0 @@
1
from ..integration.pydantic_ai import AIModel, patch_infer_model