prompt-caller 0.1.1__py3-none-any.whl → 0.2.0__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.
@@ -1,13 +1,18 @@
1
1
  import os
2
2
  import re
3
+ import ast
3
4
 
4
5
  import requests
5
6
  import yaml
6
7
  from dotenv import load_dotenv
7
8
  from jinja2 import Template
9
+ from langgraph.types import Command
8
10
  from langchain_core.tools import tool
9
11
  from langchain_core.messages import HumanMessage, SystemMessage, ToolMessage
12
+ from langchain.agents import create_agent
13
+ from langchain.agents.middleware import wrap_tool_call
10
14
  from langchain_openai import ChatOpenAI
15
+ from langchain_google_genai import ChatGoogleGenerativeAI
11
16
  from PIL import Image
12
17
  from pydantic import BaseModel, Field, create_model
13
18
 
@@ -18,7 +23,6 @@ load_dotenv()
18
23
 
19
24
 
20
25
  class PromptCaller:
21
-
22
26
  def __init__(self, promptPath="prompts"):
23
27
  self.promptPath = promptPath
24
28
 
@@ -39,17 +43,44 @@ class PromptCaller:
39
43
  template = Template(body)
40
44
  return template.render(context)
41
45
 
46
+ import re
47
+
42
48
  def _parseJSXBody(self, body):
43
49
  elements = []
44
- tag_pattern = r"<(system|user|assistant|image)>(.*?)</\1>"
50
+ # 1. Regex to find tags, attributes string, and content
51
+ tag_pattern = r"<(system|user|assistant|image)([^>]*)>(.*?)</\1>"
52
+
53
+ # 2. Regex to find key="value" pairs within the attributes string
54
+ attr_pattern = r'(\w+)\s*=\s*"(.*?)"'
45
55
 
46
56
  matches = re.findall(tag_pattern, body, re.DOTALL)
47
57
 
48
- for tag, content in matches:
49
- elements.append({"role": tag, "content": content.strip()})
58
+ for tag, attrs_string, content in matches:
59
+ # 3. Parse the attributes string (e.g., ' tag="image 1"') into a dict
60
+ attributes = {}
61
+ if attrs_string:
62
+ attr_matches = re.findall(attr_pattern, attrs_string)
63
+ for key, value in attr_matches:
64
+ attributes[key] = value
65
+
66
+ element = {"role": tag, "content": content.strip()}
67
+
68
+ # 4. Add the attributes to our element dict if they exist
69
+ if attributes:
70
+ element["attributes"] = attributes
71
+
72
+ elements.append(element)
50
73
 
51
74
  return elements
52
75
 
76
+ def _createChat(self, configuration):
77
+ if configuration.get("model") is not None and configuration.get(
78
+ "model"
79
+ ).startswith("gemini"):
80
+ return ChatGoogleGenerativeAI(**configuration)
81
+ else:
82
+ return ChatOpenAI(**configuration)
83
+
53
84
  def getImageBase64(self, url: str) -> str:
54
85
  response = requests.get(url)
55
86
  response.raise_for_status()
@@ -87,16 +118,18 @@ class PromptCaller:
87
118
  if base64_image.startswith("http"):
88
119
  base64_image = self.getImageBase64(base64_image)
89
120
 
90
- messages.append(
91
- HumanMessage(
92
- content=[
93
- {
94
- "type": "image_url",
95
- "image_url": {"url": base64_image},
96
- }
97
- ]
98
- )
99
- )
121
+ content = [
122
+ {
123
+ "type": "image_url",
124
+ "image_url": {"url": base64_image},
125
+ }
126
+ ]
127
+
128
+ tag = message.get("attributes", {}).get("tag")
129
+ if tag:
130
+ content.append({"type": "text", "text": f"({tag})"})
131
+
132
+ messages.append(HumanMessage(content=content))
100
133
 
101
134
  return configuration, messages
102
135
 
@@ -110,7 +143,6 @@ class PromptCaller:
110
143
  return create_model("DynamicModel", **fields)
111
144
 
112
145
  def call(self, promptName, context=None):
113
-
114
146
  configuration, messages = self.loadPrompt(promptName, context)
115
147
 
116
148
  output = None
@@ -119,7 +151,7 @@ class PromptCaller:
119
151
  output = configuration.get("output")
120
152
  configuration.pop("output")
121
153
 
122
- chat = ChatOpenAI(**configuration)
154
+ chat = self._createChat(configuration)
123
155
 
124
156
  if output:
125
157
  dynamicModel = self.createPydanticModel(output)
@@ -129,78 +161,123 @@ class PromptCaller:
129
161
 
130
162
  return response
131
163
 
132
- def agent(self, promptName, context=None, tools=None, allowed_steps=3):
164
+ def _create_pdf_middleware(self):
165
+ """Middleware to handle tool responses that contain pdf content."""
166
+
167
+ @wrap_tool_call
168
+ def handle_pdf_response(request, handler):
169
+ # Execute the actual tool
170
+ result = handler(request)
171
+
172
+ # Check if result content is pdf data
173
+ if hasattr(result, "content"):
174
+ content = result.content
175
+ # Try to parse if it's a string representation of a list
176
+ if isinstance(content, str) and content.startswith("["):
177
+ try:
178
+ content = ast.literal_eval(content)
179
+ except (ValueError, SyntaxError):
180
+ pass
181
+
182
+ if (
183
+ isinstance(content, list)
184
+ and content
185
+ and isinstance(content[0], dict)
186
+ and "input_file" in content[0]
187
+ and "pdf" in content[0]["file_data"]
188
+ ):
189
+ # Use Command to add both tool result and image to messages
190
+ return Command(
191
+ update={"messages": [result, HumanMessage(content=content)]}
192
+ )
133
193
 
134
- configuration, messages = self.loadPrompt(promptName, context)
194
+ return result # Return normal result
195
+
196
+ return handle_pdf_response
197
+
198
+ def _create_image_middleware(self):
199
+ """Middleware to handle tool responses that contain image content."""
200
+
201
+ @wrap_tool_call
202
+ def handle_image_response(request, handler):
203
+ # Execute the actual tool
204
+ result = handler(request)
205
+
206
+ # Check if result content is image data (list with image_url dict)
207
+ if hasattr(result, "content"):
208
+ content = result.content
209
+ # Try to parse if it's a string representation of a list
210
+ if isinstance(content, str) and content.startswith("["):
211
+ try:
212
+ content = ast.literal_eval(content)
213
+ except (ValueError, SyntaxError):
214
+ pass
215
+
216
+ if (
217
+ isinstance(content, list)
218
+ and content
219
+ and isinstance(content[0], dict)
220
+ and "image_url" in content[0]
221
+ ):
222
+ # Use Command to add both tool result and image to messages
223
+ return Command(
224
+ update={"messages": [result, HumanMessage(content=content)]}
225
+ )
135
226
 
136
- output = None
227
+ return result # Return normal result
137
228
 
138
- if "output" in configuration:
139
- output = configuration.get("output")
140
- configuration.pop("output")
229
+ return handle_image_response
141
230
 
142
- for message in messages:
143
- if isinstance(message, SystemMessage):
144
- message.content += "\nOnly use the tool DynamicModel when providing an output call."
145
- break
231
+ def agent(
232
+ self, promptName, context=None, tools=None, output=None, allowed_steps=10
233
+ ):
234
+ configuration, messages = self.loadPrompt(promptName, context)
235
+
236
+ # Handle structured output from config
237
+ dynamicOutput = None
238
+ if output is None and "output" in configuration:
239
+ dynamicOutput = configuration.pop("output")
146
240
 
147
- chat = ChatOpenAI(**configuration)
241
+ chat = self._createChat(configuration)
148
242
 
149
- # Register the tools
243
+ # Prepare tools
150
244
  if tools is None:
151
245
  tools = []
152
-
153
- # Transform functions in tools
154
246
  tools = [tool(t) for t in tools]
155
247
 
156
- tools_dict = {t.name.lower(): t for t in tools}
157
-
248
+ # Handle response format (structured output)
249
+ response_format = None
158
250
  if output:
159
- dynamicModel = self.createPydanticModel(output)
160
-
161
- tools.extend([dynamicModel])
162
- tools_dict["dynamicmodel"] = dynamicModel
163
-
164
- chat = chat.bind_tools(tools)
165
-
166
- try:
167
- # First LLM invocation
168
- response = chat.invoke(messages)
169
- messages.append(response)
170
-
171
- steps = 0
172
- while response.tool_calls and steps < allowed_steps:
173
- for tool_call in response.tool_calls:
174
- tool_name = tool_call["name"].lower()
175
-
176
- # If it's the final formatting tool, validate and return
177
- if tool_name == "dynamicmodel":
178
- return dynamicModel.model_validate(tool_call["args"])
179
-
180
- selected_tool = tools_dict.get(tool_name)
181
- if not selected_tool:
182
- raise ValueError(f"Unknown tool: {tool_name}")
183
-
184
- # Invoke the selected tool with provided arguments
185
- tool_response = selected_tool.invoke(tool_call)
186
- messages.append(tool_response)
187
-
188
- # If the latest message is a ToolMessage, re-invoke the LLM
189
- if isinstance(messages[-1], ToolMessage):
190
- response = chat.invoke(messages)
191
- messages.append(response)
192
- else:
193
- break
194
-
195
- steps += 1
196
-
197
- # Final LLM call if the last message is still a ToolMessage
198
- if isinstance(messages[-1], ToolMessage):
199
- response = chat.invoke(messages)
200
- messages.append(response)
251
+ response_format = output
252
+ elif dynamicOutput:
253
+ response_format = self.createPydanticModel(dynamicOutput)
254
+
255
+ # Extract system message for create_agent
256
+ system_prompt = None
257
+ user_messages = []
258
+ for msg in messages:
259
+ if isinstance(msg, SystemMessage):
260
+ system_prompt = msg.content
261
+ else:
262
+ user_messages.append(msg)
263
+
264
+ # Create and invoke agent
265
+ agent_graph = create_agent(
266
+ model=chat,
267
+ tools=tools,
268
+ system_prompt=system_prompt,
269
+ response_format=response_format,
270
+ middleware=[
271
+ self._create_image_middleware(),
272
+ self._create_pdf_middleware(),
273
+ ],
274
+ )
201
275
 
202
- return response
276
+ result = agent_graph.invoke(
277
+ {"messages": user_messages}, config={"recursion_limit": allowed_steps}
278
+ )
203
279
 
204
- except Exception as e:
205
- # Replace with appropriate logging in production
206
- raise RuntimeError("Error during agent process") from e
280
+ # Return structured output or last message
281
+ if response_format and result.get("structured_response"):
282
+ return result["structured_response"]
283
+ return result["messages"][-1]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: prompt_caller
3
- Version: 0.1.1
3
+ Version: 0.2.0
4
4
  Summary: This package is responsible for calling prompts in a specific format. It uses LangChain and OpenAI API
5
5
  Home-page: https://github.com/ThiNepo/prompt-caller
6
6
  Author: Thiago Nepomuceno
@@ -11,11 +11,13 @@ Classifier: Operating System :: OS Independent
11
11
  Description-Content-Type: text/markdown
12
12
  License-File: LICENSE
13
13
  Requires-Dist: pyyaml>=6.0.2
14
- Requires-Dist: python-dotenv>=1.0.1
14
+ Requires-Dist: python-dotenv>=1.2.1
15
15
  Requires-Dist: Jinja2>=3.1.4
16
- Requires-Dist: langchain-openai>=0.3.5
17
- Requires-Dist: openai>=1.63.0
18
- Requires-Dist: pillow>=11.0.0
16
+ Requires-Dist: langchain-core>=1.2.7
17
+ Requires-Dist: langchain-openai>=1.1.7
18
+ Requires-Dist: langchain-google-genai>=4.2.0
19
+ Requires-Dist: openai>=2.16.0
20
+ Requires-Dist: pillow>=12.1.0
19
21
 
20
22
  # PromptCaller
21
23
 
@@ -0,0 +1,8 @@
1
+ prompt_caller/__init__.py,sha256=4EGdeAJ_Ig7A-b-e17-nYbiXjckT7uL3to5lchMsoW4,41
2
+ prompt_caller/__main__.py,sha256=dJ0dYtVmnhZuoV79R6YiAIta1ZkUKb-TEX4VEuYbgk0,139
3
+ prompt_caller/prompt_caller.py,sha256=b6AvhCRDfSpRHpg5qGVkTV1WRwSsmq5l0uy79Y-XYEs,9798
4
+ prompt_caller-0.2.0.dist-info/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
5
+ prompt_caller-0.2.0.dist-info/METADATA,sha256=ntbB3PEOrASgd4UjhzXMSztBUIr3pdASL2R42mPNQak,4993
6
+ prompt_caller-0.2.0.dist-info/WHEEL,sha256=eOLhNAGa2EW3wWl_TU484h7q1UNgy0JXjjoqKoxAAQc,92
7
+ prompt_caller-0.2.0.dist-info/top_level.txt,sha256=iihiDRq-0VrKB8IKjxf7Lrtv-fLMq4tvgM4fH3x0I94,14
8
+ prompt_caller-0.2.0.dist-info/RECORD,,
@@ -1,8 +0,0 @@
1
- prompt_caller/__init__.py,sha256=4EGdeAJ_Ig7A-b-e17-nYbiXjckT7uL3to5lchMsoW4,41
2
- prompt_caller/__main__.py,sha256=dJ0dYtVmnhZuoV79R6YiAIta1ZkUKb-TEX4VEuYbgk0,139
3
- prompt_caller/prompt_caller.py,sha256=fy-pLXmYD2j5fnAxgBvxCNBrkQvDPGX0nWyqnaWeqSo,6737
4
- prompt_caller-0.1.1.dist-info/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
5
- prompt_caller-0.1.1.dist-info/METADATA,sha256=j1xmg_Y_NAhh7CmlCgAcfSMuHChZ9C4DPcszLADg7dk,4909
6
- prompt_caller-0.1.1.dist-info/WHEEL,sha256=eOLhNAGa2EW3wWl_TU484h7q1UNgy0JXjjoqKoxAAQc,92
7
- prompt_caller-0.1.1.dist-info/top_level.txt,sha256=iihiDRq-0VrKB8IKjxf7Lrtv-fLMq4tvgM4fH3x0I94,14
8
- prompt_caller-0.1.1.dist-info/RECORD,,