prompt-caller 0.1.3__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,12 +1,16 @@
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
11
15
  from langchain_google_genai import ChatGoogleGenerativeAI
12
16
  from PIL import Image
@@ -19,7 +23,6 @@ load_dotenv()
19
23
 
20
24
 
21
25
  class PromptCaller:
22
-
23
26
  def __init__(self, promptPath="prompts"):
24
27
  self.promptPath = promptPath
25
28
 
@@ -40,14 +43,33 @@ class PromptCaller:
40
43
  template = Template(body)
41
44
  return template.render(context)
42
45
 
46
+ import re
47
+
43
48
  def _parseJSXBody(self, body):
44
49
  elements = []
45
- 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*"(.*?)"'
46
55
 
47
56
  matches = re.findall(tag_pattern, body, re.DOTALL)
48
57
 
49
- for tag, content in matches:
50
- 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)
51
73
 
52
74
  return elements
53
75
 
@@ -96,16 +118,18 @@ class PromptCaller:
96
118
  if base64_image.startswith("http"):
97
119
  base64_image = self.getImageBase64(base64_image)
98
120
 
99
- messages.append(
100
- HumanMessage(
101
- content=[
102
- {
103
- "type": "image_url",
104
- "image_url": {"url": base64_image},
105
- }
106
- ]
107
- )
108
- )
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))
109
133
 
110
134
  return configuration, messages
111
135
 
@@ -119,7 +143,6 @@ class PromptCaller:
119
143
  return create_model("DynamicModel", **fields)
120
144
 
121
145
  def call(self, promptName, context=None):
122
-
123
146
  configuration, messages = self.loadPrompt(promptName, context)
124
147
 
125
148
  output = None
@@ -138,87 +161,123 @@ class PromptCaller:
138
161
 
139
162
  return response
140
163
 
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
+ )
193
+
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
+ )
226
+
227
+ return result # Return normal result
228
+
229
+ return handle_image_response
230
+
141
231
  def agent(
142
232
  self, promptName, context=None, tools=None, output=None, allowed_steps=10
143
233
  ):
144
-
145
234
  configuration, messages = self.loadPrompt(promptName, context)
146
235
 
236
+ # Handle structured output from config
147
237
  dynamicOutput = None
148
-
149
238
  if output is None and "output" in configuration:
150
- dynamicOutput = configuration.get("output")
151
- configuration.pop("output")
152
-
153
- for message in messages:
154
- if isinstance(message, SystemMessage):
155
- message.content += "\nOnly use the tool DynamicModel when providing an output call."
156
- break
239
+ dynamicOutput = configuration.pop("output")
157
240
 
158
241
  chat = self._createChat(configuration)
159
242
 
160
- # Register the tools
243
+ # Prepare tools
161
244
  if tools is None:
162
245
  tools = []
163
-
164
- # Transform functions in tools
165
246
  tools = [tool(t) for t in tools]
166
247
 
167
- tools_dict = {t.name.lower(): t for t in tools}
168
-
248
+ # Handle response format (structured output)
249
+ response_format = None
169
250
  if output:
170
- tools.extend([output])
171
- tools_dict[output.__name__.lower()] = output
251
+ response_format = output
172
252
  elif dynamicOutput:
173
- dynamicModel = self.createPydanticModel(dynamicOutput)
174
-
175
- tools.extend([dynamicModel])
176
- tools_dict["dynamicmodel"] = dynamicModel
177
-
178
- chat = chat.bind_tools(tools)
179
-
180
- try:
181
- # First LLM invocation
182
- response = chat.invoke(messages)
183
- messages.append(response)
184
-
185
- steps = 0
186
- while response.tool_calls and steps < allowed_steps:
187
- for tool_call in response.tool_calls:
188
- tool_name = tool_call["name"].lower()
189
-
190
- # If it's the final formatting tool, validate and return
191
- if dynamicOutput and tool_name == "dynamicmodel":
192
- return dynamicModel.model_validate(tool_call["args"])
193
-
194
- if output and tool_name == output.__name__.lower():
195
- return output.model_validate(tool_call["args"])
196
-
197
- selected_tool = tools_dict.get(tool_name)
198
- if not selected_tool:
199
- raise ValueError(f"Unknown tool: {tool_name}")
200
-
201
- # Invoke the selected tool with provided arguments
202
- tool_response = selected_tool.invoke(tool_call)
203
- messages.append(tool_response)
204
-
205
- # If the latest message is a ToolMessage, re-invoke the LLM
206
- if isinstance(messages[-1], ToolMessage):
207
- response = chat.invoke(messages)
208
- messages.append(response)
209
- else:
210
- break
211
-
212
- steps += 1
213
-
214
- # Final LLM call if the last message is still a ToolMessage
215
- if isinstance(messages[-1], ToolMessage):
216
- response = chat.invoke(messages)
217
- messages.append(response)
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
+ )
218
275
 
219
- return response
276
+ result = agent_graph.invoke(
277
+ {"messages": user_messages}, config={"recursion_limit": allowed_steps}
278
+ )
220
279
 
221
- except Exception as e:
222
- print(e)
223
- # Replace with appropriate logging in production
224
- 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.3
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=XKmYQuU4XMFeeUEPjTEukpinAlapHOB_ZgWORVb9GnI,7489
4
- prompt_caller-0.1.3.dist-info/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
5
- prompt_caller-0.1.3.dist-info/METADATA,sha256=eOke1OseFZfeHAggOBrIDbXIRLTlkUrlPV1bn8t0WUY,4909
6
- prompt_caller-0.1.3.dist-info/WHEEL,sha256=eOLhNAGa2EW3wWl_TU484h7q1UNgy0JXjjoqKoxAAQc,92
7
- prompt_caller-0.1.3.dist-info/top_level.txt,sha256=iihiDRq-0VrKB8IKjxf7Lrtv-fLMq4tvgM4fH3x0I94,14
8
- prompt_caller-0.1.3.dist-info/RECORD,,