Steve commited on
Commit
876f9d3
·
unverified ·
2 Parent(s): 647f6f2 9a20d2b

Merge pull request #1 from east-and-west-magic/configure

Browse files
Files changed (9) hide show
  1. app.py +40 -84
  2. app_util.py +0 -154
  3. call_ai.py +0 -37
  4. call_logger.py +33 -0
  5. call_pgai.py +18 -18
  6. extract.py +6 -12
  7. requirements-local.txt +1 -58
  8. requirements.txt +1 -58
  9. str_util.py → utils.py +0 -0
app.py CHANGED
@@ -5,17 +5,15 @@ from zoneinfo import ZoneInfo
5
 
6
  import gradio as gr
7
  from extract import extract
8
- import app_util
9
- from pgsoft.pgconst.const import service_list, functionality_list
10
- from pgsoft.pghost import ais
11
  from pgsoft.pgdate.date_utils import beijing
12
  import call_pgai
13
- from str_util import normalize_text
14
 
15
  #######################
16
  # proxy version
17
  #######################
18
- proxy_version = "1.0.0-2024-05-14-a"
19
 
20
  t = datetime.now()
21
  t = t.astimezone(ZoneInfo("Asia/Shanghai"))
@@ -26,83 +24,52 @@ print(f"[Seattle]: {t.replace(microsecond=0)}")
26
 
27
  identity = os.environ.get("identity")
28
  print(f"identity: {identity}")
29
- if not identity:
30
- identity = "watermelon"
31
- ai = "stevez-ai-dev"
32
- if identity in ais:
33
- ai = ais[identity]
34
  db_token = os.environ.get("db_token")
35
  if db_token:
36
  print(db_token[:5])
37
 
 
 
 
 
 
 
38
 
39
- def run(hf_token, service, game, functionality, nlp_command):
40
- """
41
- event handler
42
- """
43
 
 
 
44
  # reuse hf_token field as json string
45
- token, user, redirect, source, username, _ = extract(hf_token)
46
- if user is None:
47
- user = "__fake__"
48
-
49
- # redirect all traffic to the proxy sever
50
- global ai
51
- if redirect is not None:
52
- ai = redirect
53
- ai_url = f"https://{ai}.hf.space"
54
- if token is None or token == "":
55
- return "please specify hf token"
56
-
57
- if service not in service_list[1:]:
58
- if game is None:
59
- return "please specify which game"
60
- if functionality is None:
61
- return "please choose the AI functionality"
62
- if functionality == "AI":
63
- if nlp_command in ["", None]:
64
- return "please make sure the command is not empty"
65
 
66
  service_start = beijing()
67
- print(f"<<<<<<<<<<<<<< service starts at {service_start} <<<<<<<<<<<<<<")
68
- if service in ["download game", "upload game", "list games"]:
69
- res = app_util.file_service(service, nlp_command, db_token)
70
- if res is None:
71
- outp = {"status": "Failure"}
72
- else:
73
- outp = {"status": "OK", "result": res}
74
- else:
75
- assert "games" in service_list
76
- if service == "games":
77
- print(f"{beijing()} [{user}] [{game}] {nlp_command}")
78
- nlp_command = normalize_text(nlp_command)
79
- call_pgai.from_cache = True
80
- outp = call_pgai.call_pgai(
81
- service,
82
- game,
83
- functionality,
84
- nlp_command,
85
- ai_url,
86
- token,
87
- )
88
- if outp is None:
89
- return "no output"
90
- if isinstance(outp, str):
91
- return outp
92
- # add proxy version info to the output
93
- outp["timestamp"] = beijing().__str__()
94
- outp["proxy-version"] = proxy_version
95
- outp["user"] = user
96
- outp["username"] = username
97
- outp["game"] = game
98
- outp["source"] = source
99
- outp["cache"] = call_pgai.from_cache
100
- app_util.call_logger(outp, identity, token)
101
  service_end = beijing()
102
  timecost = service_end.timestamp() - service_start.timestamp()
103
  print(
104
- f">>>>>>>>>>>>>>> service ends at {service_end}, "
105
- + f"costs {timecost:.2f}s >>>>>>>>>>>>>>>\n"
106
  )
107
  return json.dumps(outp, indent=4)
108
 
@@ -112,25 +79,14 @@ demo = gr.Interface(
112
  inputs=[
113
  "text",
114
  gr.Radio(
115
- service_list,
116
- value=service_list[0],
117
- info="Shared services",
118
- ),
119
- gr.Radio(
120
- ["house"],
121
- value="house",
122
  info="Which game you want the AI to support?",
123
  ),
124
- gr.Radio(
125
- functionality_list[:1],
126
- value=functionality_list[0],
127
- # label = "What do you want to do?",
128
- info="What functionality?",
129
- ),
130
  "text",
131
  ],
132
  outputs="text",
133
- title="Demo",
134
  allow_flagging="never",
135
  )
136
 
 
5
 
6
  import gradio as gr
7
  from extract import extract
8
+ import call_logger
 
 
9
  from pgsoft.pgdate.date_utils import beijing
10
  import call_pgai
11
+ from utils import normalize_text
12
 
13
  #######################
14
  # proxy version
15
  #######################
16
+ proxy_version = "1.0.0-2024-07-30-a"
17
 
18
  t = datetime.now()
19
  t = t.astimezone(ZoneInfo("Asia/Shanghai"))
 
24
 
25
  identity = os.environ.get("identity")
26
  print(f"identity: {identity}")
 
 
 
 
 
27
  db_token = os.environ.get("db_token")
28
  if db_token:
29
  print(db_token[:5])
30
 
31
+ game_list = [
32
+ "matchn",
33
+ "house",
34
+ "watermelon",
35
+ "snake",
36
+ ]
37
 
 
 
 
 
38
 
39
+ def run(info, game, nlp_command):
40
+ """event handler"""
41
  # reuse hf_token field as json string
42
+ user, source, username, _ = extract(info)
43
+ if nlp_command is None:
44
+ return "command is required"
45
+ nlp_command = normalize_text(nlp_command)
46
+ if nlp_command == "":
47
+ return "invalid command"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
  service_start = beijing()
50
+ print(f"[{service_start}] service starts >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")
51
+ print(f"[{user}] [{game}] [{nlp_command}]")
52
+
53
+ call_pgai.from_cache = True
54
+ outp = call_pgai.call_pgai(nlp_command, game)
55
+ if outp is None:
56
+ return "no output"
57
+ if isinstance(outp, str):
58
+ return outp
59
+ # add proxy version info to the output
60
+ outp["timestamp"] = beijing().__str__()
61
+ outp["proxy-version"] = proxy_version
62
+ outp["user"] = user
63
+ outp["username"] = username
64
+ outp["game"] = game
65
+ outp["source"] = source
66
+ outp["cache"] = call_pgai.from_cache
67
+ call_logger.call_logger(outp, identity, db_token)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  service_end = beijing()
69
  timecost = service_end.timestamp() - service_start.timestamp()
70
  print(
71
+ f"[{service_end}] service ends, costs {timecost:.2f}s "
72
+ + "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n\n"
73
  )
74
  return json.dumps(outp, indent=4)
75
 
 
79
  inputs=[
80
  "text",
81
  gr.Radio(
82
+ game_list,
83
+ value=game_list[0],
 
 
 
 
 
84
  info="Which game you want the AI to support?",
85
  ),
 
 
 
 
 
 
86
  "text",
87
  ],
88
  outputs="text",
89
+ title="Pgai",
90
  allow_flagging="never",
91
  )
92
 
app_util.py DELETED
@@ -1,154 +0,0 @@
1
- import json
2
-
3
- from gradio_client import Client
4
- from pgsoft.pgdate.date_utils import beijing
5
- from pgsoft.pgfile import download, upload, list_files
6
- from pgsoft.pghash.md5 import md5
7
- from time import sleep
8
- from huggingface_hub import HfApi
9
- import os
10
-
11
-
12
- def call_logger(log_info, caller, hf_token) -> None:
13
- #######################
14
- # logging
15
- #######################
16
- calling_start = beijing()
17
- print(f"calling logger starts at {beijing()}")
18
- #################################################
19
- urls = [
20
- "https://hubei-hunan-logger.hf.space",
21
- "https://hubei-hunan-logger2.hf.space",
22
- ]
23
- for url in urls:
24
- try:
25
- client = Client(
26
- url,
27
- hf_token=hf_token,
28
- verbose=False,
29
- )
30
- client.submit(json.dumps(log_info), caller)
31
- print(f"[logging to {url}] OK")
32
- except Exception as e:
33
- print(f"[logging to {url}] error: {e}")
34
- #################################################
35
- calling_end = beijing()
36
- timecost = calling_end.timestamp() - calling_start.timestamp()
37
- print(f"calling logger ends at {calling_end}, costs {timecost:.2f}s")
38
-
39
-
40
- dataset_id = "pgsoft/game"
41
- tempdir = "game"
42
- gamename = "house"
43
- localdir = os.path.join(tempdir, gamename)
44
- if not os.path.exists(localdir):
45
- os.makedirs(localdir)
46
- hf_api = HfApi()
47
-
48
-
49
- def file_service(service, arg: str, token: str):
50
- """download game, upload game, or list games"""
51
- if service == "download game":
52
- filename = arg.strip() + ".json"
53
- remotepath = "/".join(["house", filename[:2], filename])
54
- res = download(
55
- dataset_id,
56
- remotepath=remotepath,
57
- repo_type="dataset",
58
- localdir=tempdir,
59
- token=token,
60
- )
61
- if not res:
62
- return None
63
- with open(res, "r") as f:
64
- outp = json.load(f)
65
- print(f"[{service}] OK")
66
- return outp
67
- elif service == "upload game":
68
- try:
69
- game = json.loads(arg)
70
- except json.JSONDecodeError as e:
71
- print(f"[{service}] {type(e)}: {e}")
72
- return None
73
-
74
- if not isinstance(game, dict):
75
- print(f"[{service}] not a dict")
76
- return None
77
-
78
- needed_keys = ["game-file", "device-id"]
79
- for key in needed_keys:
80
- if key not in game:
81
- print(f'[{service}] error: missed "{key}"')
82
- return None
83
- if not isinstance(game["device-id"], str):
84
- print(f'[{service}] error: "device-id" is not a str')
85
- return None
86
- if not isinstance(game["game-file"], dict):
87
- print(f'[{service}] error: "game-file" is not a dict')
88
- return None
89
-
90
- obj = {
91
- "upload-time": beijing().__str__(),
92
- "game-file": game["game-file"],
93
- }
94
-
95
- maxtry = 5
96
- for retry in range(maxtry):
97
- md5code = md5(obj)
98
- remotepath = "/".join([gamename, md5code[:2], md5code + ".json"])
99
- if not hf_api.file_exists(
100
- repo_id=dataset_id,
101
- filename=remotepath,
102
- repo_type="dataset",
103
- token=token,
104
- ):
105
- break
106
- sleep(0.1)
107
- obj["upload-time"] = beijing().__str__()
108
- maxtry -= 1
109
- if not maxtry and hf_api.file_exists(
110
- repo_id=dataset_id,
111
- filename=remotepath,
112
- repo_type="dataset",
113
- token=token,
114
- ):
115
- print(f"[{service}] error: file exists")
116
- return None
117
- filedir = os.path.join(localdir, md5code[:2])
118
- if not os.path.exists(filedir):
119
- os.mkdir(filedir)
120
- filepath = os.path.join(filedir, md5code + ".json")
121
- content = json.dumps(game, indent=4)
122
- with open(filepath, "w") as f:
123
- f.write(content)
124
- res = upload(
125
- filepath,
126
- remotepath,
127
- dataset_id,
128
- "dataset",
129
- token,
130
- f"Updated at {beijing()}",
131
- )
132
- if not res:
133
- print(f"[{service}] error: upload failed")
134
- return None
135
- print(f"[{service}] OK")
136
- return md5code
137
- elif service == "list games":
138
- games = list_files(
139
- repo_id=dataset_id,
140
- repo_type="dataset",
141
- token=token,
142
- )
143
- if games is None:
144
- return None
145
- games = {
146
- item.split(".")[0][-32:]: item
147
- for item in games
148
- if item.endswith(".json") and item.startswith("house")
149
- }
150
- print(f"[{service}] OK")
151
- return games
152
- else:
153
- print(f"[{service}] error: unknown service")
154
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
call_ai.py DELETED
@@ -1,37 +0,0 @@
1
- from functools import cache
2
- from gradio_client import Client
3
- from pgsoft.pgdate.date_utils import beijing
4
- import json
5
-
6
- from_cache = True
7
-
8
-
9
- @cache
10
- def call_ai(service, game, functionality, nlp_command, url, hf_token):
11
- calling_start = beijing()
12
- print(f"calling ai starts at {calling_start}")
13
- try:
14
- client = Client(
15
- url,
16
- hf_token=hf_token,
17
- verbose=False,
18
- )
19
- res = client.predict(
20
- service,
21
- game,
22
- functionality,
23
- nlp_command, # hidden,
24
- api_name="/predict",
25
- )
26
- except Exception as e:
27
- return (
28
- f"{type(e)}, {str(e)}. \nyou may want to make "
29
- + "sure your hf_token is correct"
30
- )
31
- calling_end = beijing()
32
- timecost = calling_end.timestamp() - calling_start.timestamp()
33
- print(f"calling ai ends at {calling_end}, costs {timecost:.2f}s")
34
- outp = json.loads(res)
35
- global from_cache
36
- from_cache = False
37
- return outp
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
call_logger.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+
3
+ from gradio_client import Client
4
+ from pgsoft.pgdate.date_utils import beijing
5
+
6
+
7
+ def call_logger(log_info, caller, hf_token) -> None:
8
+ #######################
9
+ # logging
10
+ #######################
11
+ calling_start = beijing()
12
+ print(f"[{calling_start}] logging starts")
13
+ #################################################
14
+ urls = [
15
+ "https://hubei-hunan-logger.hf.space",
16
+ "https://hubei-hunan-logger2.hf.space",
17
+ ]
18
+ for url in urls:
19
+ try:
20
+ client = Client(
21
+ url,
22
+ hf_token=hf_token,
23
+ verbose=False,
24
+ )
25
+ client.submit(json.dumps(log_info), caller)
26
+ print(f"[logging to {url}] OK")
27
+ return None
28
+ except Exception as e:
29
+ print(f"[logging to {url}] Failed, {type(e)}: {e}")
30
+ #################################################
31
+ calling_end = beijing()
32
+ timecost = calling_end.timestamp() - calling_start.timestamp()
33
+ print(f"[{calling_end}] logging ends, costs {timecost:.2f}s")
call_pgai.py CHANGED
@@ -6,14 +6,14 @@ from pgsoft.pgdate.date_utils import beijing
6
  import json
7
 
8
 
9
- def post_to_pgai_helper(command: str) -> Any:
10
  myobj = {
11
- "code": os.getenv("pgai_code"),
12
  "command": command,
13
  }
14
 
15
- header = {'accept': 'application/json', 'Content-Type': 'application/json'}
16
- url_base = "https://steveagi-pgai.hf.space/games/house/nlp"
17
  url_read = f"{url_base}/r"
18
 
19
  try:
@@ -27,27 +27,27 @@ def post_to_pgai_helper(command: str) -> Any:
27
  except Exception as e:
28
  print(e)
29
  return None
30
-
31
 
32
  from_cache = True
 
 
33
  # @cache
34
- def call_pgai(service, game, functionality, nlp_command, url, hf_token):
35
  calling_start = beijing()
36
- print(f"calling ai starts at {calling_start}")
37
  try:
38
- res = post_to_pgai_helper(nlp_command)
39
  except Exception as e:
40
- return (
41
- f"{type(e)}, {str(e)}. \nyou may want to make "
42
- + "sure your hf_token is correct"
43
- )
44
  calling_end = beijing()
45
  timecost = calling_end.timestamp() - calling_start.timestamp()
46
- print(f"calling ai ends at {calling_end}, costs {timecost:.2f}s")
47
  global from_cache
48
  from_cache = False
49
- if res is not None:
50
- outp = json.loads(res)
51
- return outp
52
- else:
53
- return None
 
6
  import json
7
 
8
 
9
+ def post_to_pgai_helper(command: str, game: str) -> Any:
10
  myobj = {
11
+ "code": os.getenv("pgai_code"),
12
  "command": command,
13
  }
14
 
15
+ header = {"accept": "application/json", "Content-Type": "application/json"}
16
+ url_base = f"https://steveagi-pgai.hf.space/games/{game}/nlp"
17
  url_read = f"{url_base}/r"
18
 
19
  try:
 
27
  except Exception as e:
28
  print(e)
29
  return None
30
+
31
 
32
  from_cache = True
33
+
34
+
35
  # @cache
36
+ def call_pgai(nlp_command, game):
37
  calling_start = beijing()
38
+ print(f"[{calling_start}] calling ai starts")
39
  try:
40
+ res = post_to_pgai_helper(nlp_command, game)
41
  except Exception as e:
42
+ return f"{type(e)}: {str(e)}."
43
+
 
 
44
  calling_end = beijing()
45
  timecost = calling_end.timestamp() - calling_start.timestamp()
46
+ print(f"[{calling_end}] calling ai ends, costs {timecost:.2f}s")
47
  global from_cache
48
  from_cache = False
49
+ if res is None:
50
+ return res
51
+ outp = json.loads(res)
52
+ return outp
53
+
extract.py CHANGED
@@ -1,20 +1,14 @@
1
  import json
2
 
3
 
4
- def extract(hf_token):
5
- """
6
- Extract token, user, redirect, source, username and info from input hf_token.
7
- If hf_token is simple, it is the token itself.
8
- """
9
  info = {} # a copy of hf_token in json format
10
  try:
11
- info = dict(json.loads(hf_token))
12
  except json.decoder.JSONDecodeError:
13
- return hf_token, None, None, None, None,None
14
- token = info.get("token", None)
15
- user = info.get("user", None)
16
- redirect = info.get("redirect", None)
17
  source = info.get("source", None)
18
  username = info.get("username", None)
19
-
20
- return token, user, redirect, source, username, info
 
1
  import json
2
 
3
 
4
+ def extract(info_str):
5
+ """Extract user, source, username, and all info"""
 
 
 
6
  info = {} # a copy of hf_token in json format
7
  try:
8
+ info = dict(json.loads(info_str))
9
  except json.decoder.JSONDecodeError:
10
+ return "__fake__", None, None, None
11
+ user = info.get("user", "__fake__")
 
 
12
  source = info.get("source", None)
13
  username = info.get("username", None)
14
+ return user, source, username, info
 
requirements-local.txt CHANGED
@@ -1,60 +1,3 @@
1
- aiofiles==23.2.1
2
- altair==5.1.2
3
- annotated-types==0.6.0
4
- anyio==3.7.1
5
- attrs==23.1.0
6
- certifi==2023.7.22
7
- charset-normalizer==3.3.0
8
- click==8.1.7
9
- contourpy==1.1.1
10
- cycler==0.12.1
11
- fastapi==0.103.2
12
- ffmpy==0.3.1
13
- # filelock==3.12.4
14
- fonttools==4.43.1
15
- fsspec==2023.9.2
16
  gradio==4.39.0
17
- gradio_client==0.6.0
18
- h11==0.14.0
19
- httpcore==0.18.0
20
- httpx==0.25.0
21
- huggingface-hub==0.18.0
22
- idna==3.4
23
- importlib-resources==6.1.0
24
- iniconfig==2.0.0
25
- Jinja2==3.1.2
26
- jsonschema==4.19.1
27
- jsonschema-specifications==2023.7.1
28
- kiwisolver==1.4.5
29
- MarkupSafe==2.1.3
30
- matplotlib==3.8.0
31
- numpy==1.26.0
32
- orjson==3.9.9
33
- packaging==23.2
34
- pandas==2.1.1
35
- Pillow==10.0.1
36
- pluggy==1.3.0
37
- pydantic==2.4.2
38
- pydantic_core==2.10.1
39
- pydub==0.25.1
40
- pyparsing==3.1.1
41
- pytest==7.4.2
42
- python-dateutil==2.8.2
43
- python-multipart==0.0.6
44
- pytz==2023.3.post1
45
- PyYAML==6.0.1
46
- referencing==0.30.2
47
- requests==2.31.0
48
- rpds-py==0.10.6
49
- semantic-version==2.10.0
50
- six==1.16.0
51
- sniffio==1.3.0
52
- starlette==0.27.0
53
- toolz==0.12.0
54
- tqdm==4.66.1
55
- typing_extensions==4.8.0
56
- tzdata==2023.3
57
- urllib3==2.0.6
58
- uvicorn==0.23.2
59
- websockets==11.0.3
60
  git+ssh://git@github.com/east-and-west-magic/pgsoft.git@tag-2024-01-11-a
 
1
+ pytest
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  gradio==4.39.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  git+ssh://git@github.com/east-and-west-magic/pgsoft.git@tag-2024-01-11-a
requirements.txt CHANGED
@@ -1,60 +1,3 @@
1
- aiofiles==23.2.1
2
- altair==5.1.2
3
- annotated-types==0.6.0
4
- anyio==3.7.1
5
- attrs==23.1.0
6
- certifi==2023.7.22
7
- charset-normalizer==3.3.0
8
- click==8.1.7
9
- contourpy==1.1.1
10
- cycler==0.12.1
11
- fastapi==0.103.2
12
- ffmpy==0.3.1
13
- # filelock==3.12.4
14
- fonttools==4.43.1
15
- fsspec==2023.9.2
16
  gradio==4.39.0
17
- gradio_client==0.6.0
18
- h11==0.14.0
19
- httpcore==0.18.0
20
- httpx==0.25.0
21
- huggingface-hub==0.18.0
22
- idna==3.4
23
- importlib-resources==6.1.0
24
- iniconfig==2.0.0
25
- Jinja2==3.1.2
26
- jsonschema==4.19.1
27
- jsonschema-specifications==2023.7.1
28
- kiwisolver==1.4.5
29
- MarkupSafe==2.1.3
30
- matplotlib==3.8.0
31
- numpy==1.26.0
32
- orjson==3.9.9
33
- packaging==23.2
34
- pandas==2.1.1
35
- Pillow==10.0.1
36
- pluggy==1.3.0
37
- pydantic==2.4.2
38
- pydantic_core==2.10.1
39
- pydub==0.25.1
40
- pyparsing==3.1.1
41
- pytest==7.4.2
42
- python-dateutil==2.8.2
43
- python-multipart==0.0.6
44
- pytz==2023.3.post1
45
- PyYAML==6.0.1
46
- referencing==0.30.2
47
- requests==2.31.0
48
- rpds-py==0.10.6
49
- semantic-version==2.10.0
50
- six==1.16.0
51
- sniffio==1.3.0
52
- starlette==0.27.0
53
- toolz==0.12.0
54
- tqdm==4.66.1
55
- typing_extensions==4.8.0
56
- tzdata==2023.3
57
- urllib3==2.0.6
58
- uvicorn==0.23.2
59
- websockets==11.0.3
60
  git+https://github.com/east-and-west-magic/pgsoft.git@tag-2024-01-11-a
 
1
+ pytest
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  gradio==4.39.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  git+https://github.com/east-and-west-magic/pgsoft.git@tag-2024-01-11-a
str_util.py → utils.py RENAMED
File without changes