GJDutils 0.2.2__py3-none-any.whl

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.
Files changed (49) hide show
  1. gjdutils/__init__.py +12 -0
  2. gjdutils/audios.py +39 -0
  3. gjdutils/cacheing.py +237 -0
  4. gjdutils/cmd.py +149 -0
  5. gjdutils/colab.py +39 -0
  6. gjdutils/collections.py +36 -0
  7. gjdutils/decorators.py +34 -0
  8. gjdutils/dicts.py +216 -0
  9. gjdutils/dsci.py +202 -0
  10. gjdutils/dt.py +296 -0
  11. gjdutils/env.py +64 -0
  12. gjdutils/errors.py +12 -0
  13. gjdutils/files.py +140 -0
  14. gjdutils/functions.py +6 -0
  15. gjdutils/google_translate.py +80 -0
  16. gjdutils/hashing.py +32 -0
  17. gjdutils/html.py +87 -0
  18. gjdutils/indexing.py +97 -0
  19. gjdutils/iterfunc.py +99 -0
  20. gjdutils/jsons.py +70 -0
  21. gjdutils/lists.py +13 -0
  22. gjdutils/llm_utils.py +167 -0
  23. gjdutils/llms_claude.py +131 -0
  24. gjdutils/llms_openai.py +299 -0
  25. gjdutils/misc.py +30 -0
  26. gjdutils/num.py +77 -0
  27. gjdutils/obsolete/google_text_to_speech.py +46 -0
  28. gjdutils/obsolete/llms_obsolete.py +298 -0
  29. gjdutils/outloud_text_to_speech.py +230 -0
  30. gjdutils/prompt_templates.py +20 -0
  31. gjdutils/pypi_build.py +112 -0
  32. gjdutils/pytest_utils.py +24 -0
  33. gjdutils/rand.py +65 -0
  34. gjdutils/regex.py +78 -0
  35. gjdutils/requirements_dev.txt +2 -0
  36. gjdutils/runtime.py +19 -0
  37. gjdutils/sets.py +5 -0
  38. gjdutils/shell.py +69 -0
  39. gjdutils/sorteddict.py +34 -0
  40. gjdutils/stopwatch.py +79 -0
  41. gjdutils/strings.py +218 -0
  42. gjdutils/todo/convert_parquet.py +28 -0
  43. gjdutils/typ.py +37 -0
  44. gjdutils/voice_speechrecognition.py +29 -0
  45. gjdutils/web.py +68 -0
  46. gjdutils-0.2.2.dist-info/METADATA +101 -0
  47. gjdutils-0.2.2.dist-info/RECORD +49 -0
  48. gjdutils-0.2.2.dist-info/WHEEL +4 -0
  49. gjdutils-0.2.2.dist-info/licenses/LICENSE +21 -0
gjdutils/lists.py ADDED
@@ -0,0 +1,13 @@
1
+ def get_list_from_str_or_list(str_or_list: str | list[str]) -> list[str]:
2
+ """
3
+ e.g.
4
+ - get_list_from_str_or_list("asdf") -> ["asdf"]
5
+ - get_list_from_str_or_list(["asdf"]) -> ["asdf"]
6
+ """
7
+ if isinstance(str_or_list, str):
8
+ aliases = [str_or_list]
9
+ elif isinstance(str_or_list, list):
10
+ aliases = str_or_list
11
+ else:
12
+ raise Exception(f"Unknown typ: {type(str_or_list)}")
13
+ return aliases
gjdutils/llm_utils.py ADDED
@@ -0,0 +1,167 @@
1
+ import base64
2
+ import io
3
+ import json
4
+ from typing import Any, Literal, Optional
5
+
6
+ from anthropic import Anthropic
7
+ from openai import OpenAI
8
+
9
+ from gjdutils.llms_claude import call_claude_gpt
10
+ from gjdutils.llms_openai import call_openai_gpt
11
+ from gjdutils.strings import jinja_render
12
+
13
+
14
+ MODEL_TYPE = Literal["openai", "claude"]
15
+
16
+
17
+ def proc_llm_out_json(s: str):
18
+ # TODO probably we don't need this function any more, because OpenAI will guarantee JSON output
19
+ """
20
+ If GPT-4 returns json output like this:
21
+
22
+ ```json
23
+ ...
24
+ ```
25
+
26
+ This strips away that markdown wrapping.
27
+
28
+ Alternatively, consider using llm_prompt_json()
29
+ """
30
+ s = s.strip()
31
+ # remove the markdown code wrapping
32
+ if s.startswith("```json") and s.endswith("```"):
33
+ s = s[7:-3]
34
+ try:
35
+ j = json.loads(s)
36
+ except:
37
+ print(s)
38
+ raise Exception("Could not parse JSON from LLM output")
39
+ return j
40
+
41
+
42
+ def image_to_base64_resized(image_full_filen: str, resize_target_size_kb: int = 100):
43
+ from PIL import Image
44
+
45
+ # based on https://claude.ai/chat/d0eb1f39-3f42-4cb5-a2ec-5aa102c60ea0
46
+ assert resize_target_size_kb > 0
47
+ with Image.open(image_full_filen) as img_orig:
48
+ width_orig, height_orig = img_orig.size
49
+ # Calculate initial file size
50
+ temp_buffer = io.BytesIO()
51
+ img_orig.save(temp_buffer, format=img_orig.format)
52
+ img_resized = img_orig.copy()
53
+ current_size_kb = len(temp_buffer.getvalue()) / 1024
54
+
55
+ # Iteratively resize until file size is below target
56
+ resize_factor = 0.9
57
+ while current_size_kb > resize_target_size_kb:
58
+ resize_factor *= resize_factor
59
+ width = int(width_orig * resize_factor)
60
+ height = int(height_orig * resize_factor)
61
+ img_resized = img_orig.resize(
62
+ (width, height), Image.LANCZOS # type: ignore
63
+ ) #  type: ignore
64
+ # Check new file size
65
+ temp_buffer = io.BytesIO()
66
+ img_resized.save(temp_buffer, format=img_orig.format)
67
+ current_size_kb = len(temp_buffer.getvalue()) / 1024
68
+
69
+ # Convert final resized image to base64
70
+ img_bytes = io.BytesIO()
71
+ img_resized.save(img_bytes, format=img_orig.format)
72
+ img_buffer = img_bytes.getvalue()
73
+
74
+ return img_buffer
75
+
76
+
77
+ def image_to_base64(img_full_filen: str, resize_target_size_kb: Optional[int] = None):
78
+ # from https://chat.openai.com/c/35f15af9-b947-4fa6-acbe-2a5ed26e7547
79
+ if resize_target_size_kb is None:
80
+ with open(img_full_filen, "rb") as image_file:
81
+ img_bytes = image_file.read()
82
+ else:
83
+ img_bytes = image_to_base64_resized(img_full_filen, resize_target_size_kb)
84
+ img_b64 = base64.b64encode(img_bytes).decode("utf-8")
85
+ return img_b64
86
+
87
+
88
+ def image_to_base64_basic(image_filen: str) -> str:
89
+ with open(image_filen, "rb") as image_file:
90
+ return base64.b64encode(image_file.read()).decode("ascii")
91
+
92
+
93
+ def contents_for_images(image_filens: list[str], resize_target_size_kb: int):
94
+ # assert (
95
+ # 1 <= len(image_filens) <= 10
96
+ # ), "You can only provide between 1 and 10 images"
97
+ base64_images = []
98
+ new_contents = []
99
+ for image_filen in image_filens:
100
+ base64_image = image_to_base64(
101
+ image_filen, resize_target_size_kb=resize_target_size_kb
102
+ )
103
+ filen_content = {
104
+ "type": "text",
105
+ "text": f"Filename: {image_filen}",
106
+ }
107
+ img_content = {
108
+ "type": "image_url",
109
+ "image_url": {"url": f"data:image/jpeg;base64,{base64_image}"},
110
+ }
111
+ base64_images.append(base64_image)
112
+ new_contents.extend([filen_content, img_content])
113
+ return new_contents, base64_images
114
+
115
+
116
+ def generate_gpt_from_template(
117
+ client: Anthropic | OpenAI,
118
+ prompt_template_var: str,
119
+ context_d: dict,
120
+ response_json: bool,
121
+ image_filens: list[str] | str | None = None,
122
+ model_type: MODEL_TYPE = "claude",
123
+ max_tokens: Optional[int] = None,
124
+ prompt_template_filen: str = "prompt_templates", # i.e. prompt_templates.py
125
+ verbose: int = 0,
126
+ ) -> tuple[str | dict[str, Any], dict[str, Any]]:
127
+ """
128
+ e.g.
129
+ generate_gpt_from_template("quick_search_for_word", {"word_tgt": word}, True, verbose)
130
+ """
131
+ # dynamically import `template_var` from prompt_templates as `prompt_template`
132
+ prompt_template = getattr(__import__(prompt_template_filen), prompt_template_var)
133
+ prompt = jinja_render(prompt_template, context_d)
134
+ if model_type == "openai":
135
+ assert isinstance(client, OpenAI), "Expected OpenAI client"
136
+ out, _, extra = call_openai_gpt(
137
+ prompt,
138
+ client=client,
139
+ image_filens=image_filens,
140
+ response_json=response_json,
141
+ max_tokens=max_tokens,
142
+ )
143
+ else:
144
+ assert isinstance(client, Anthropic), "Expected Anthropic client"
145
+ out, extra = call_claude_gpt(
146
+ prompt,
147
+ client=client,
148
+ image_filens=image_filens,
149
+ response_json=response_json,
150
+ max_tokens=max_tokens if max_tokens is not None else 4096,
151
+ )
152
+ if response_json:
153
+ assert isinstance(out, dict), f"Expected dict, got {type(out)}"
154
+ else:
155
+ assert isinstance(out, str), f"Expected str, got {type(out)}"
156
+ if verbose >= 1:
157
+ print(
158
+ f"Called GPT on '{prompt_template_var}', context keys {list(context_d.keys())}"
159
+ )
160
+ extra.update(
161
+ {
162
+ "model_type": model_type,
163
+ "prompt_template": prompt_template_var,
164
+ "prompt_context_d": context_d,
165
+ }
166
+ )
167
+ return out, extra # type: ignore
@@ -0,0 +1,131 @@
1
+ import json
2
+ from pathlib import Path
3
+ from anthropic import Anthropic, NOT_GIVEN
4
+ import os
5
+ from typing import Optional
6
+
7
+ from gjdutils.rand import DEFAULT_RANDOM_SEED
8
+ from gjdutils.llm_utils import image_to_base64_basic
9
+
10
+ CLAUDE_API_KEY = os.environ.get("CLAUDE_API_KEY")
11
+ # https://docs.anthropic.com/en/docs/about-claude/models
12
+ MODEL_NAME_CLAUDE_SONNET_GOOD_PINNED = "claude-3-5-sonnet-20240620"
13
+ MODEL_NAME_CLAUDE_SONNET_GOOD_LATEST = "claude-3-5-sonnet-latest"
14
+ MODEL_NAME_CLAUDE_SONNET_CHEAP_LATEST = "claude-3-5-haiku-latest"
15
+
16
+
17
+ def img_as_content_dict(img_filen: str):
18
+ media_type_from_extension = {
19
+ ".jpg": "image/jpeg",
20
+ ".jpeg": "image/jpeg",
21
+ ".png": "image/png",
22
+ ".webp": "image/webp",
23
+ ".gif": "image/gif",
24
+ ".bmp": "image/bmp",
25
+ ".tiff": "image/tiff",
26
+ ".ico": "image/vnd.microsoft.icon",
27
+ ".svg": "image/svg+xml",
28
+ ".heic": "image/heic",
29
+ ".heif": "image/heif",
30
+ }
31
+
32
+ ext = Path(img_filen).suffix
33
+ if ext not in media_type_from_extension:
34
+ raise ValueError(f"Unknown image file extension: {img_filen}")
35
+ media_type = media_type_from_extension[ext]
36
+
37
+ img_base64 = image_to_base64_basic(img_filen)
38
+ return {
39
+ "type": "image",
40
+ "source": {
41
+ "type": "base64",
42
+ "media_type": media_type,
43
+ "data": img_base64,
44
+ },
45
+ }
46
+
47
+
48
+ def call_claude_gpt(
49
+ prompt: str,
50
+ tools: Optional[list[dict]] = None,
51
+ image_filens: str | list[str] | None = None,
52
+ image_resize_target_size_kb: Optional[int] = 100,
53
+ client: Optional[Anthropic] = None,
54
+ model: str = MODEL_NAME_CLAUDE_SONNET_GOOD_LATEST,
55
+ temperature: Optional[float] = 0.001,
56
+ response_json: bool = False,
57
+ # seed: Optional[int] = DEFAULT_RANDOM_SEED,
58
+ max_tokens: int = 4096,
59
+ verbose: int = 0,
60
+ ):
61
+ """Call Claude API with support for text, images, and function calling"""
62
+ extra = locals()
63
+ extra.pop("client")
64
+
65
+ if tools is not None:
66
+ raise NotImplementedError(
67
+ "I think tools are supported, but not implemented in this function"
68
+ )
69
+
70
+ if client is None:
71
+ client = Anthropic(api_key=CLAUDE_API_KEY)
72
+
73
+ # Prepare image contents if provided
74
+ contents = []
75
+ if image_filens:
76
+ if isinstance(image_filens, str):
77
+ image_filens = [image_filens]
78
+ assert image_resize_target_size_kb is not None
79
+ for i, img_filen in enumerate(image_filens):
80
+ contents.extend(
81
+ [
82
+ {"type": "text", "text": f"Image {i+1}:"},
83
+ img_as_content_dict(image_filens[i]),
84
+ # {
85
+ # "type": "image",
86
+ # "source": {
87
+ # "type": "base64",
88
+ # "media_type": "image/jpeg", # Adjust based on actual image type
89
+ # "data": b64,
90
+ # },
91
+ # },
92
+ ]
93
+ )
94
+
95
+ # Add text prompt
96
+ contents.append({"type": "text", "text": prompt})
97
+
98
+ # Prepare response format
99
+ response_format = {"type": "json_object"} if response_json else None
100
+
101
+ # Make API call
102
+ response = client.messages.create(
103
+ model=model,
104
+ max_tokens=max_tokens,
105
+ messages=[{"role": "user", "content": contents}],
106
+ temperature=temperature if temperature is not None else NOT_GIVEN,
107
+ # seed=seed,
108
+ # response_format=response_format,
109
+ )
110
+
111
+ msg = response.content[0].text # type: ignore
112
+ if response_json:
113
+ msg = json.loads(msg)
114
+ extra.update(
115
+ {
116
+ "response": response.model_dump(),
117
+ "msg": msg,
118
+ # "tool_calls": tool_calls,
119
+ "model": model,
120
+ "contents": contents,
121
+ }
122
+ )
123
+
124
+ if verbose >= 2:
125
+ print(f"PROMPT:\n{prompt}")
126
+ if verbose >= 1:
127
+ print(f"LLM MESSAGE:\n{msg}")
128
+ if verbose >= 2:
129
+ # print(f"TOOL CALLS:\n{tool_calls}")
130
+ print(f"LLM RESPONSE:\n{json.dumps(response.model_dump(), indent=2)}")
131
+ return msg, extra
@@ -0,0 +1,299 @@
1
+ import json
2
+ from openai import OpenAI, NOT_GIVEN
3
+ import os
4
+ from pprint import pprint
5
+ from typing import Any, Literal, Optional
6
+
7
+ from .prompt_templates import summarise_list_of_texts_as_one, summarise_text
8
+ from .rand import DEFAULT_RANDOM_SEED
9
+ from .strings import jinja_render
10
+ from gjdutils.llm_utils import contents_for_images
11
+
12
+
13
+ # openai.api_key = OPENAI_API_KEY
14
+ OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
15
+ MODEL_NAME_GPT4 = "gpt-4"
16
+ MODEL_NAME_GPT35 = "gpt-3.5-turbo"
17
+ MODEL_NAME_GPT4_TURBO = "gpt-4-turbo" # -1106-preview"
18
+ MODEL_NAME_GPT4O = "gpt-4o"
19
+ MODEL_NAME_GPT4O_MINI = "gpt-4o-mini"
20
+ MODEL_NAME_O1 = "o1-preview"
21
+ MODEL_NAME_O1_MINI = "o1-mini"
22
+ DEFAULT_MODEL_NAME = MODEL_NAME_GPT4O
23
+
24
+ MODELS_NO_TOOLS = [MODEL_NAME_O1, MODEL_NAME_O1_MINI]
25
+
26
+ # from https://github.com/openai/openai-python
27
+ ToolChoiceTyps = Literal["auto", "required", "none"]
28
+
29
+ GranularityTyps = Literal[
30
+ "short phrase of just a few words",
31
+ "short title",
32
+ "short sentence",
33
+ "at most a sentence",
34
+ "sentence or two",
35
+ "few sentences",
36
+ "single short paragraph",
37
+ "couple of paragraphs",
38
+ "page",
39
+ ]
40
+
41
+
42
+ def call_openai_gpt(
43
+ prompt: str,
44
+ tools: Optional[list[dict]] = None,
45
+ tool_choice: Optional[ToolChoiceTyps] = None,
46
+ image_filens: str | list[str] | None = None,
47
+ image_resize_target_size_kb: Optional[int] = 100,
48
+ client: Optional[OpenAI] = None,
49
+ model: Optional[str] = None,
50
+ temperature: Optional[float] = 0.001,
51
+ max_tokens: int | None = None,
52
+ # stop: Optional[list[str]] = None,
53
+ response_json: bool = False,
54
+ seed: Optional[int] = DEFAULT_RANDOM_SEED,
55
+ verbose: int = 0,
56
+ ):
57
+ """
58
+ Usage:
59
+
60
+ client = OpenAI(
61
+ api_key=OPENAI_API_KEY,
62
+ )
63
+ msg, tools, extra = call_gpt_uncached(client, "What is the capital of France?")
64
+
65
+ @cachier()
66
+ def call_gpt_cached(
67
+ inp: str, tools: Optional[list[dict]] = None, tool_choice: ToolChoiceTyps = "auto"
68
+ ):
69
+ return call_gpt(client=client, inp=inp, tools=tools, tool_choice=tool_choice)
70
+
71
+ https://platform.openai.com/docs/api-reference/chat/create?lang=python
72
+ SAMPLE_TOOLS = [
73
+ {
74
+ "type": "function",
75
+ "function": {
76
+ "name": "get_current_weather",
77
+ "description": "Get the current weather in a given location",
78
+ "parameters": {
79
+ "type": "object",
80
+ "properties": {
81
+ "location": {
82
+ "type": "string",
83
+ "description": "The city and state, e.g. San Francisco, CA",
84
+ },
85
+ "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
86
+ },
87
+ "required": ["location"],
88
+ },
89
+ },
90
+ }
91
+ ]
92
+
93
+ The messages for images could look like this:
94
+ {
95
+ "role": "user",
96
+ "content": [
97
+ {
98
+ "type": "text",
99
+ "text": "Filename: cat.jpg"
100
+ },
101
+ {
102
+ "type": "image_url",
103
+ "image_url": {
104
+ "url": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAA..."
105
+ }
106
+ },
107
+ {
108
+ "type": "text",
109
+ "text": "Filename: dog.png"
110
+ },
111
+ {
112
+ "type": "image_url",
113
+ "image_url": {
114
+ "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJA..."
115
+ }
116
+ },
117
+ {
118
+ "type": "text",
119
+ "text": "Describe each of these images, referring to them by their filenames:"
120
+ },
121
+ ]
122
+ }
123
+ ]
124
+ """
125
+
126
+ extra = locals()
127
+ extra.pop("client") # to avoid caching issues, and because it includes the API key
128
+ if client is None:
129
+ client = OpenAI(api_key=OPENAI_API_KEY)
130
+ if not tools:
131
+ # otherwise you get a 400
132
+ tool_choice = None
133
+ if model is None:
134
+ model = DEFAULT_MODEL_NAME
135
+ if image_filens is None:
136
+ base64_images = None
137
+ image_contents = []
138
+ else:
139
+ if isinstance(image_filens, str):
140
+ image_filens = [image_filens]
141
+ assert (
142
+ image_resize_target_size_kb is not None
143
+ ), "You must provide a resize_target_size_kb"
144
+ image_contents, base64_images = contents_for_images(
145
+ image_filens, resize_target_size_kb=image_resize_target_size_kb
146
+ )
147
+
148
+ prompt_content = {"type": "text", "text": prompt}
149
+ contents = image_contents + [prompt_content]
150
+ messages = [{"role": "user", "content": contents}]
151
+ response_format = {"type": "json_object"} if response_json else None
152
+ if model in MODELS_NO_TOOLS:
153
+ assert (
154
+ tools is None
155
+ ), "You cannot provide tools for models that don't support them"
156
+ assert (
157
+ tool_choice is None
158
+ ), "You cannot provide tool_choice for models that don't support them"
159
+ tools, tool_choice = NOT_GIVEN, NOT_GIVEN # type: ignore
160
+ # assert temperature is None, f"Temperature can't be set for {model}"
161
+ temperature = NOT_GIVEN # type: ignore
162
+ response = client.chat.completions.create(
163
+ model=model,
164
+ messages=messages, # type: ignore
165
+ tools=tools, # type: ignore
166
+ tool_choice=tool_choice, # type: ignore
167
+ temperature=temperature,
168
+ max_tokens=max_tokens if max_tokens is not None else NOT_GIVEN,
169
+ # stop=stop, # for some reason, setting this to None causes an error
170
+ seed=seed,
171
+ response_format=response_format, # type: ignore
172
+ )
173
+ msg = response.choices[0].message.content # could be empty
174
+ if response_json:
175
+ msg = json.loads(msg) # type: ignore
176
+ tool_calls = response.choices[0].message.tool_calls # could be None or a list
177
+ extra.update(
178
+ {
179
+ "response": response.model_dump(),
180
+ "msg": msg,
181
+ "tool_calls": tool_calls,
182
+ "model": model,
183
+ "base64_images": base64_images,
184
+ "contents": contents,
185
+ # "client": client,
186
+ }
187
+ )
188
+ if verbose >= 2:
189
+ print(f"PROMPT:\n{prompt}")
190
+ if verbose >= 1:
191
+ print(f"LLM MESSAGE:\n{msg}")
192
+ if verbose >= 2:
193
+ print(f"TOOL CALLS:\n{tool_calls}")
194
+ print(f"LLM RESPONSE:\n{json.dumps(response.model_dump(), indent=2)}")
195
+ return msg, tool_calls, extra
196
+
197
+
198
+ def llm_generate_summary(
199
+ txt_or_txts: str | list[str],
200
+ granularity: Optional[GranularityTyps] = None,
201
+ full_txt_or_html: Optional[str] = None,
202
+ n_truncate_words=None,
203
+ model_name: Optional[str] = None,
204
+ max_tokens: Optional[int] = None,
205
+ verbose: int = 0,
206
+ ):
207
+ """
208
+ TXT_OR_TXTS can either be a single string,
209
+ or a list of strings (in which case it tries to find the summary that unifies them).
210
+
211
+ FULL_TXT_OR_HTML is the full text or HTML that the text is a part of.
212
+ It is used to provide context to the summarisation.
213
+
214
+ TODO: I combined summarisation of text and list into one, but I'm not convinced
215
+ it was such a good idea. It has made things unwieldy. I'm mostly focused on the
216
+ summarisation of a single text for now.
217
+
218
+ TODO maybe we don't need both MAX_TOKENS and N_TRUNCATE_WORDS. Maybe we can just
219
+ use MAX_TOKENS and calculate N_TRUNCATE_WORDS from that.
220
+ """
221
+
222
+ def do_summarise_text(txt: str):
223
+ if n_truncate_words:
224
+ txt = txt[:n_truncate_words]
225
+ context["txt"] = txt
226
+ prompt = jinja_render(summarise_text, context)
227
+ extra.update(
228
+ {
229
+ "txt": txt, # type: ignore
230
+ }
231
+ ) # type: ignore
232
+ return prompt
233
+
234
+ def do_summarise_list(txts: list[str]):
235
+ # UNTESTED
236
+ txts = [txt.replace("\n", " ").replace(" ", " ").strip() for txt in txts]
237
+ if max_tokens is not None:
238
+ if n_truncate_words is None: # type: ignore
239
+ # assume a word is <1.5 tokens. so 3500 / 10 / 1.5 = 233
240
+ n_truncate_words = int(max_tokens / len(txts) / 1.5)
241
+ if n_truncate_words is not None: # type: ignore
242
+ txts = [txt[:n_truncate_words] for txt in txts if txt] # type: ignore
243
+ context["txts"] = txts # type: ignore
244
+ prompt = jinja_render(summarise_list_of_texts_as_one, context)
245
+ extra.update(
246
+ {
247
+ "txts": txts,
248
+ "max_tokens": max_tokens,
249
+ "n_truncate_words": n_truncate_words, # type: ignore
250
+ }
251
+ ) # type: ignore
252
+ return prompt
253
+
254
+ extra = {"input": locals()}
255
+ if full_txt_or_html is not None:
256
+ raise NotImplementedError("full_txt_or_html is not yet implemented")
257
+ context = {
258
+ "granularity": (
259
+ "Adjust the length of your summary appropriately, based on the length and complexity of the text. For example, if the text is a paragraph, write a sentence or two. If it's a page, write a paragraph or so. If it's a book, write a page."
260
+ if granularity is None
261
+ else f"Write at most a {granularity}."
262
+ )
263
+ }
264
+ assert txt_or_txts, "txt_or_txts must be non-empty"
265
+ if isinstance(txt_or_txts, str):
266
+ prompt = do_summarise_text(txt=txt_or_txts)
267
+ elif isinstance(txt_or_txts, list):
268
+ prompt = do_summarise_list(txts=txt_or_txts)
269
+ else:
270
+ raise TypeError("txt_or_txts must be str or list[str]: %s" % type(txt_or_txts))
271
+
272
+ assert max_tokens is None, "max_tokens is not yet implemented"
273
+ msg, tools, extra = call_openai_gpt(
274
+ prompt=prompt, model=model_name
275
+ ) # , max_tokens=max_tokens)
276
+ extra.update(
277
+ {
278
+ "context": context,
279
+ "prompt": prompt,
280
+ "llm_msg": msg,
281
+ "llm_tools": tools,
282
+ "llm_extra": extra,
283
+ } # type: ignore
284
+ )
285
+ if verbose > 0:
286
+ print("Summary:", msg)
287
+ if verbose > 1:
288
+ print(f"PROMPT:\n{prompt}")
289
+ extra = {
290
+ "model_name": model_name,
291
+ "prompt": prompt,
292
+ }
293
+ return msg, extra
294
+
295
+
296
+ if __name__ == "__main__":
297
+ # txt = prompt('What is the capital of France?')
298
+ msg, _, _ = call_openai_gpt("What is the capital of France?")
299
+ print(msg)
gjdutils/misc.py ADDED
@@ -0,0 +1,30 @@
1
+ from .dicts import print_dict
2
+ from .typ import isfunction
3
+
4
+
5
+ def print_locals(
6
+ d: dict, ignore_functions: bool = True, ignore_underscores: bool = True
7
+ ):
8
+ """
9
+ e.g. print_locals(locals())
10
+ """
11
+
12
+ def del_robust(k):
13
+ if k in d:
14
+ del d[k]
15
+
16
+ assert isinstance(d, dict)
17
+ for k in d.keys():
18
+ if ignore_functions and isfunction(d[k]):
19
+ del_robust(k)
20
+ if ignore_underscores and k.startswith("_"):
21
+ del_robust(k)
22
+ return print_dict(d)
23
+
24
+
25
+ def identity_func(x):
26
+ return x
27
+
28
+
29
+ def empty_func(*args, **kwargs):
30
+ return None