Skip to content

Commit 0e91640

Browse files
committed
- Dynamic provider g4fauto. Resolves #29
- Test and save working g4f providers. #29 ```sh pytgpt gpt4free test -y ``` - Order providers in ascending. Resolves #31
1 parent 5045d59 commit 0e91640

File tree

6 files changed

+296
-5
lines changed

6 files changed

+296
-5
lines changed

docs/CHANGELOG.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,4 +366,15 @@ For instance:
366366

367367
**What's new?**
368368

369-
- New model : **GPT4ALL** - Support offline LLM.
369+
- New model : **GPT4ALL** - Support offline LLM.
370+
371+
## v0.4.6
372+
373+
**What's new?**
374+
375+
- Dynamic provider `g4fauto`. #29
376+
- Test and save working g4f providers . #29
377+
```sh
378+
pytgpt gpt4free test -y
379+
```
380+
- Order providers in ascending. #31

docs/README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@ These are simply the hosts of the LLMs, which include:
8181

8282
41+ Other models proudly offered by [gpt4free](https://github.com/xtekky/gpt4free).
8383

84+
- To list working providers run:
85+
```sh
86+
$ pytgpt gpt4free test -y
87+
```
88+
8489
</summary>
8590

8691
- AiChatOnline
@@ -529,7 +534,7 @@ This can be useful in some ways. For instance :
529534

530535
## Passing Environment Variables
531536

532-
Pytgpt **v0.4.6** onwards introduces a convention way of taking variables from the environment.
537+
Pytgpt **v0.4.6** introduces a convention way of taking variables from the environment.
533538
To achieve that, set the environment variables in your operating system or script with prefix `PYTGPT_` followed by the option name in uppercase, replacing dashes with underscores.
534539

535540
For example, for the option `--provider`, you would set an environment variable `PYTGPT_PROVIDER` to provide a default value for that option. Same case applies to boolean flags such as `--rawdog` whose environment variable will be `PYTGPT_RAWDOG` with value being either `true/false`. Finally, `--awesome-prompt` will take the environment variable `PYTGPT_AWESOME_PROMPT`.
@@ -538,6 +543,10 @@ The environment variables can be overridden by explicitly declaring new value.
538543

539544
> **Note** : This is not limited to any command.
540545
546+
## Dynamic Provider
547+
548+
Version **0.4.6** also introduces dynamic provider called `g4fauto`, which represents the fastest working g4f-based provider.
549+
541550
For more usage info run `$ pytgpt --help`
542551

543552
</summary>

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636

3737
setup(
3838
name="python-tgpt",
39-
version="0.4.5",
39+
version="0.4.6",
4040
license="MIT",
4141
author="Smartwa",
4242
maintainer="Smartwa",

src/pytgpt/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from .utils import appdir
22
import g4f
33

4-
__version__ = "0.4.5"
4+
__version__ = "0.4.6"
55
__author__ = "Smartwa"
66
__repo__ = "https://github.com/Simatwa/python-tgpt"
77

@@ -16,6 +16,7 @@
1616
"blackboxai",
1717
"gpt4all",
1818
"webchatgpt",
19+
"g4fauto",
1920
]
2021

2122
gpt4free_providers = [

src/pytgpt/console.py

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,32 @@ def __init__(
362362
intro = self.RawDog.intro_prompt
363363
getpass.getuser = lambda: "RawDog"
364364

365-
if provider == "leo":
365+
if provider == "g4fauto":
366+
from pytgpt.gpt4free.utils import TestProviders
367+
368+
test = TestProviders(quiet=quiet, timeout=timeout)
369+
g4fauto = test.best if ignore_working else test.auto
370+
if isinstance(g4fauto, str):
371+
provider = "g4fauto+" + g4fauto
372+
from pytgpt.gpt4free import GPT4FREE
373+
374+
self.bot = GPT4FREE(
375+
provider=g4fauto,
376+
auth=auth,
377+
max_tokens=max_tokens,
378+
model=model,
379+
chat_completion=chat_completion,
380+
ignore_working=ignore_working,
381+
timeout=timeout,
382+
intro=intro,
383+
filepath=filepath,
384+
update_file=update_file,
385+
proxies=proxies,
386+
history_offset=history_offset,
387+
act=awesome_prompt,
388+
)
389+
390+
elif provider == "leo":
366391
import pytgpt.leo as leo
367392

368393
self.bot = leo.LEO(
@@ -1892,6 +1917,7 @@ def show(target, working, url, stream, context, gpt35, gpt4, json):
18921917
if hunted_providers[0] is None
18931918
else hunted_providers
18941919
)
1920+
hunted_providers.sort()
18951921
if json:
18961922
rich.print_json(data=dict(providers=hunted_providers), indent=4)
18971923

@@ -1986,6 +2012,72 @@ def gui(port, address, debug, open):
19862012
click.launch(f"http://{address}:{port}")
19872013
t1.join()
19882014

2015+
@staticmethod
2016+
@click.command(context_settings=this.context_settings)
2017+
@click.option(
2018+
"-t",
2019+
"--timeout",
2020+
type=click.INT,
2021+
help="Provider response generation tiemout",
2022+
default=20,
2023+
)
2024+
@click.option(
2025+
"-r",
2026+
"--thread",
2027+
type=click.INT,
2028+
help="Test n amount of providers at once",
2029+
default=5,
2030+
)
2031+
@click.option("-q", "--quiet", is_flag=True, help="Suppress all stdout")
2032+
@click.option(
2033+
"-j", "--json", is_flag=True, help="Stdout test results in json format"
2034+
)
2035+
@click.option("-d", "--dry-test", is_flag=True, help="Return previous test results")
2036+
@click.option(
2037+
"-b", "--best", is_flag=True, help="Stdout the fastest provider <name only>"
2038+
)
2039+
@click.option("-y", "--yes", is_flag=True, help="Okay to all confirmations")
2040+
@click.help_option("-h", "--help")
2041+
def test(timeout, thread, quiet, json, dry_test, best, yes):
2042+
"""Test and save working providers"""
2043+
from pytgpt.gpt4free import utils
2044+
2045+
test = utils.TestProviders(test_at_once=thread, quiet=quiet, timeout=timeout)
2046+
if best:
2047+
click.secho(test.best)
2048+
return
2049+
elif dry_test:
2050+
results = test.get_results(
2051+
run=False,
2052+
)
2053+
else:
2054+
if (
2055+
yes
2056+
or os.path.isfile(utils.results_path)
2057+
and click.confirm("Are you sure to run new test")
2058+
):
2059+
results = test.get_results(run=True)
2060+
else:
2061+
results = test.get_results(
2062+
run=False,
2063+
)
2064+
if json:
2065+
rich.print_json(data=dict(results=results))
2066+
else:
2067+
table = Table(
2068+
title="G4f Providers Test Results",
2069+
show_lines=True,
2070+
)
2071+
table.add_column("No.", style="white", justify="center")
2072+
table.add_column("Provider", style="yellow", justify="left")
2073+
table.add_column("Response Time(s)", style="cyan")
2074+
2075+
for no, provider in enumerate(results, start=1):
2076+
table.add_row(
2077+
str(no), provider["name"], str(round(provider["time"], 2))
2078+
)
2079+
rich.print(table)
2080+
19892081

19902082
class Utils:
19912083
"""Utilities command"""
@@ -2072,6 +2164,7 @@ def make_commands():
20722164
EntryGroup.gpt4free.add_command(Gpt4free.update)
20732165
EntryGroup.gpt4free.add_command(Gpt4free.show)
20742166
EntryGroup.gpt4free.add_command(Gpt4free.gui)
2167+
EntryGroup.gpt4free.add_command(Gpt4free.test)
20752168

20762169
# Awesome
20772170
EntryGroup.awesome.add_command(Awesome.add)

src/pytgpt/gpt4free/utils.py

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import g4f
2+
from .main import GPT4FREE
3+
from pathlib import Path
4+
from pytgpt.utils import default_path
5+
from json import dump, load
6+
from time import time
7+
from threading import Thread as thr
8+
from functools import wraps
9+
import datetime
10+
from rich.progress import Progress
11+
import click
12+
import logging
13+
14+
results_path = Path(default_path) / "provider_test.json"
15+
16+
17+
def exception_handler(func):
18+
19+
@wraps(func)
20+
def decorator(*args, **kwargs):
21+
try:
22+
return func(*args, **kwargs)
23+
except Exception as e:
24+
pass
25+
26+
return decorator
27+
28+
29+
@exception_handler
30+
def is_working(provider: str) -> bool:
31+
"""Test working status of a provider
32+
33+
Args:
34+
provider (str): Provider name
35+
36+
Returns:
37+
bool: is_working status
38+
"""
39+
bot = GPT4FREE(provider=provider, is_conversation=False)
40+
text = bot.chat("hello")
41+
assert isinstance(text, str)
42+
assert bool(text.strip())
43+
assert "<" not in text
44+
assert len(text) > 2
45+
return True
46+
47+
48+
class TestProviders:
49+
50+
def __init__(self, test_at_once: int = 5, quiet: bool = False, timeout: int = 20):
51+
"""Constructor
52+
53+
Args:
54+
test_at_once (int, optional): Test n providers at once. Defaults to 5.
55+
quiet (bool, optinal): Disable stdout. Defaults to False.
56+
timout (int, optional): Thread timeout for each provider. Defaults to 20.
57+
"""
58+
self.test_at_once: int = test_at_once
59+
self.quiet = quiet
60+
self.timeout = timeout
61+
self.working_providers: list = [
62+
provider.__name__
63+
for provider in g4f.Provider.__providers__
64+
if provider.working
65+
]
66+
self.results_path: Path = results_path
67+
self.__create_empty_file(ignore_if_found=True)
68+
69+
def __create_empty_file(self, ignore_if_found: bool = False):
70+
if ignore_if_found and self.results_path.is_file():
71+
return
72+
with self.results_path.open("w") as fh:
73+
dump({"results": []}, fh)
74+
75+
def test_provider(self, name: str):
76+
"""Test each provider and save successful ones
77+
78+
Args:
79+
name (str): Provider name
80+
"""
81+
82+
try:
83+
bot = GPT4FREE(provider=name, is_conversation=False)
84+
start_time = time()
85+
text = bot.chat("hello there")
86+
assert isinstance(text, str), "Non-string response returned"
87+
assert bool(text.strip()), "Empty string"
88+
assert "<" not in text, "Html code returned."
89+
assert len(text) > 2
90+
except Exception as e:
91+
pass
92+
else:
93+
with self.results_path.open() as fh:
94+
current_results = load(fh)
95+
new_result = dict(time=time() - start_time, name=name)
96+
current_results["results"].append(new_result)
97+
logging.info(f"Test result - {new_result['name']} - {new_result['time']}")
98+
99+
with self.results_path.open("w") as fh:
100+
dump(current_results, fh)
101+
102+
@exception_handler
103+
def main(
104+
self,
105+
):
106+
self.__create_empty_file()
107+
threads = []
108+
# Create a progress bar
109+
total = len(self.working_providers)
110+
with Progress() as progress:
111+
logging.info(f"Testing {total} providers : {self.working_providers}")
112+
task = progress.add_task(
113+
f"[cyan]Testing...[{self.test_at_once}]",
114+
total=total,
115+
visible=self.quiet == False,
116+
)
117+
while not progress.finished:
118+
for count, provider in enumerate(self.working_providers, start=1):
119+
t1 = thr(
120+
target=self.test_provider,
121+
args=(provider,),
122+
)
123+
t1.start()
124+
if count % self.test_at_once == 0 or count == len(provider):
125+
for t in threads:
126+
try:
127+
t.join(self.timeout)
128+
except Exception as e:
129+
pass
130+
threads.clear()
131+
else:
132+
threads.append(t1)
133+
progress.update(task, advance=1)
134+
135+
def get_results(self, run: bool = False, best: bool = False) -> list[dict]:
136+
"""Get test results
137+
138+
Args:
139+
run (bool, optional): Run the test first. Defaults to False.
140+
best (bool, optional): Return name of the best provider. Defaults to False.
141+
142+
Returns:
143+
list[dict]|str: Test results.
144+
"""
145+
if run:
146+
self.main()
147+
148+
with self.results_path.open() as fh:
149+
results: dict = load(fh)
150+
151+
results = results["results"]
152+
time_list = []
153+
154+
sorted_list = []
155+
for entry in results:
156+
time_list.append(entry["time"])
157+
158+
time_list.sort()
159+
160+
for time_value in time_list:
161+
for entry in results:
162+
if entry["time"] == time_value:
163+
sorted_list.append(entry)
164+
return sorted_list[0]["name"] if best else sorted_list
165+
166+
@property
167+
def best(self):
168+
"""Fastest provider overally"""
169+
return self.get_results(run=False, best=True)
170+
171+
@property
172+
def auto(self):
173+
"""Best working provider"""
174+
for result in self.get_results(run=False, best=False):
175+
logging.info("Confirming working status of provider : " + result["name"])
176+
if is_working(result["name"]):
177+
return result["name"]

0 commit comments

Comments
 (0)