RafaelJaime commited on
Commit
417d69b
·
2 Parent(s): 7b49e90 26e31ab

Merge branch 'main' of https://huggingface.co/spaces/AgentsGuards/agents-guard-mcp

Browse files
.gitignore CHANGED
@@ -1 +1,3 @@
1
- __pycache__/
 
 
 
1
+ __pycache__/
2
+ .env
3
+ test_agent.py
gradio_interface/app.py CHANGED
@@ -1,7 +1,204 @@
 
1
  import gradio as gr
 
 
 
 
 
 
 
 
2
 
3
- def greet(name):
4
- return "Hello " + name + "!!"
 
5
 
6
- demo = gr.Interface(fn=greet, inputs="text", outputs="text")
7
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
  import gradio as gr
3
+ from os import getenv
4
+ import base64
5
+ from io import BytesIO
6
+ from dotenv import load_dotenv
7
+ import requests
8
+ import socket
9
+ import logging
10
+ import json
11
 
12
+ from langchain_openai import ChatOpenAI
13
+ from langchain_core.messages import HumanMessage, AIMessage
14
+ from langchain_core.callbacks import StreamingStdOutCallbackHandler
15
 
16
+ # Load environment
17
+ dotenv_path = os.path.join(os.path.dirname(__file__), '.env')
18
+ load_dotenv(dotenv_path=dotenv_path)
19
+
20
+ # Connectivity test
21
+ def test_connectivity(url="https://openrouter.helicone.ai/api/v1"):
22
+ try:
23
+ return requests.get(url, timeout=5).status_code == 200
24
+ except (requests.RequestException, socket.error):
25
+ return False
26
+
27
+ # Helper to make direct API calls to OpenRouter when LangChain fails
28
+ def direct_api_call(messages, api_key, base_url):
29
+ headers = {
30
+ "Content-Type": "application/json",
31
+ "Authorization": f"Bearer {api_key}",
32
+ "HTTP-Referer": "https://your-app-domain.com", # Add your domain
33
+ "X-Title": "Image Analysis App"
34
+ }
35
+
36
+ if getenv("HELICONE_API_KEY"):
37
+ headers["Helicone-Auth"] = f"Bearer {getenv('HELICONE_API_KEY')}"
38
+
39
+ payload = {
40
+ "model": "google/gemini-flash-1.5",
41
+ "messages": messages,
42
+ "stream": False,
43
+ }
44
+
45
+ try:
46
+ response = requests.post(
47
+ f"{base_url}/chat/completions",
48
+ headers=headers,
49
+ json=payload,
50
+ timeout=30
51
+ )
52
+ response.raise_for_status()
53
+ return response.json()["choices"][0]["message"]["content"]
54
+ except Exception as e:
55
+ return f"Error: {str(e)}"
56
+
57
+ # Initialize LLM with streaming and retry logic
58
+ def init_llm():
59
+ if not test_connectivity():
60
+ raise RuntimeError("No hay conexión a OpenRouter. Verifica red y claves.")
61
+ return ChatOpenAI(
62
+ openai_api_key=getenv("OPENROUTER_API_KEY"),
63
+ openai_api_base=getenv("OPENROUTER_BASE_URL"),
64
+ model_name="google/gemini-flash-1.5",
65
+ streaming=True,
66
+ callbacks=[StreamingStdOutCallbackHandler()],
67
+ model_kwargs={
68
+ "extra_headers": {"Helicone-Auth": f"Bearer {getenv('HELICONE_API_KEY')}"}
69
+ },
70
+ )
71
+
72
+ # Try to initialize LLM but handle failures gracefully
73
+ try:
74
+ llm = init_llm()
75
+ except Exception as e:
76
+ llm = None
77
+
78
+ # Helpers
79
+ def encode_image_to_base64(pil_image):
80
+ buffer = BytesIO()
81
+ pil_image.save(buffer, format="PNG")
82
+ return base64.b64encode(buffer.getvalue()).decode()
83
+
84
+ # Core logic
85
+ def generate_response(message, chat_history, image):
86
+ # Convert chat history to standard format
87
+ formatted_history = []
88
+ for msg in chat_history:
89
+ role = msg.get('role')
90
+ content = msg.get('content')
91
+ if role == 'user':
92
+ formatted_history.append({"role": "user", "content": content})
93
+ else:
94
+ formatted_history.append({"role": "assistant", "content": content})
95
+
96
+ # Prepare system message
97
+ system_msg = {"role": "system", "content": "You are an expert image analysis assistant. Answer succinctly."}
98
+
99
+ # Prepare the latest message with image if provided
100
+ if image:
101
+ base64_image = encode_image_to_base64(image)
102
+
103
+ # Format for direct API call (OpenRouter/OpenAI format)
104
+ api_messages = [system_msg] + formatted_history + [{
105
+ "role": "user",
106
+ "content": [
107
+ {"type": "text", "text": message},
108
+ {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{base64_image}"}}
109
+ ]
110
+ }]
111
+
112
+ # For LangChain format
113
+ content_for_langchain = [
114
+ {"type": "text", "text": message},
115
+ {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{base64_image}"}}
116
+ ]
117
+ else:
118
+ api_messages = [system_msg] + formatted_history + [{"role": "user", "content": message}]
119
+ content_for_langchain = message
120
+
121
+ # Build LangChain messages
122
+ lc_messages = [HumanMessage(content="You are an expert image analysis assistant. Answer succinctly.")]
123
+ for msg in chat_history:
124
+ role = msg.get('role')
125
+ content = msg.get('content')
126
+ if role == 'user':
127
+ lc_messages.append(HumanMessage(content=content))
128
+ else:
129
+ lc_messages.append(AIMessage(content=content))
130
+
131
+ lc_messages.append(HumanMessage(content=content_for_langchain))
132
+
133
+ try:
134
+ # First try with LangChain
135
+ if llm:
136
+ try:
137
+ try:
138
+ stream_iter = llm.stream(lc_messages)
139
+ partial = ""
140
+ for chunk in stream_iter:
141
+ if chunk is None:
142
+ continue
143
+ content = getattr(chunk, 'content', None)
144
+ if content is None:
145
+ continue
146
+ partial += content
147
+ yield partial
148
+
149
+ # If we got this far, streaming worked
150
+ return
151
+ except Exception as e:
152
+ print(f"Streaming failed: {e}. Falling back to non-streaming mode")
153
+
154
+ # Try non-streaming
155
+ try:
156
+ response = llm.invoke(lc_messages)
157
+ yield response.content
158
+ return
159
+ except Exception as e:
160
+ raise e
161
+ except Exception as e:
162
+ raise e
163
+
164
+ response_text = direct_api_call(
165
+ api_messages,
166
+ getenv("OPENROUTER_API_KEY"),
167
+ getenv("OPENROUTER_BASE_URL")
168
+ )
169
+ yield response_text
170
+
171
+ except Exception as e:
172
+ import traceback
173
+ error_trace = traceback.format_exc()
174
+ yield f"⚠️ Error al generar respuesta: {str(e)}. Intenta más tarde."
175
+
176
+ # Gradio interface
177
+ def process_message(message, chat_history, image):
178
+ if chat_history is None:
179
+ chat_history = []
180
+ if image is None:
181
+ chat_history.append({'role':'assistant','content':'Por favor sube una imagen.'})
182
+ return "", chat_history
183
+ chat_history.append({'role':'user','content':message})
184
+ chat_history.append({'role':'assistant','content':'⏳ Procesando...'})
185
+ yield "", chat_history
186
+ for chunk in generate_response(message, chat_history, image):
187
+ chat_history[-1]['content'] = chunk
188
+ yield "", chat_history
189
+ return "", chat_history
190
+
191
+ with gr.Blocks() as demo:
192
+ with gr.Row():
193
+ with gr.Column(scale=2):
194
+ chatbot = gr.Chatbot(type='messages', height=600)
195
+ msg = gr.Textbox(label="Mensaje", placeholder="Escribe tu pregunta...")
196
+ clear = gr.ClearButton([msg, chatbot])
197
+ with gr.Column(scale=1):
198
+ image_input = gr.Image(type="pil", label="Sube Imagen")
199
+ info = gr.Textbox(label="Info Imagen", interactive=False)
200
+
201
+ msg.submit(process_message, [msg, chatbot, image_input], [msg, chatbot])
202
+ image_input.change(lambda img: f"Tamaño: {img.size}" if img else "Sin imagen.", [image_input], [info])
203
+
204
+ demo.launch()
mcp_server.py CHANGED
@@ -5,6 +5,7 @@ from src.utils.visualize_image import visualize_base64_image
5
  from src.utils.generate_image import generate_image
6
  from src.utils.apply_filter import apply_filter
7
  from src.utils.add_text import add_text_to_image
 
8
  from src.utils.watermark import add_watermark, remove_watermark
9
  from src.utils.describe import describe_image
10
  from src.utils.compress import compress_image
 
5
  from src.utils.generate_image import generate_image
6
  from src.utils.apply_filter import apply_filter
7
  from src.utils.add_text import add_text_to_image
8
+ from src.utils.resize_image import resize_image
9
  from src.utils.watermark import add_watermark, remove_watermark
10
  from src.utils.describe import describe_image
11
  from src.utils.compress import compress_image
src/utils/change_format.py CHANGED
@@ -2,8 +2,9 @@ from PIL import Image
2
  from io import BytesIO
3
  import requests
4
  import base64
 
5
 
6
- def change_format(image_url: str, target_format: str) -> str:
7
  """
8
  Change the format of an image from a URL to the specified target format.
9
 
@@ -15,18 +16,30 @@ def change_format(image_url: str, target_format: str) -> str:
15
  The image converted to the target format as a base64-encoded string.
16
  """
17
 
18
- response = requests.get(image_url, timeout=30)
19
- response.raise_for_status()
 
20
 
21
- # Open the image from bytes
22
- img = Image.open(BytesIO(response.content))
23
 
24
  # Convert the image to the target format
25
- output = BytesIO()
26
- img.save(output, format=target_format)
27
- output.seek(0)
28
-
29
- # Convert to base64 string for JSON serialization
30
- encoded_image = base64.b64encode(output.getvalue()).decode('utf-8')
31
-
32
- return encoded_image # Return base64 encoded string that can be serialized to JSON
 
 
 
 
 
 
 
 
 
 
 
 
2
  from io import BytesIO
3
  import requests
4
  import base64
5
+ from typing import Union
6
 
7
+ def change_format(image: Union[str, BytesIO], target_format: str) -> str:
8
  """
9
  Change the format of an image from a URL to the specified target format.
10
 
 
16
  The image converted to the target format as a base64-encoded string.
17
  """
18
 
19
+ if not isinstance(image, BytesIO):
20
+ response = requests.get(image, timeout=30)
21
+ response.raise_for_status()
22
 
23
+ # Open the image from bytes
24
+ img = Image.open(BytesIO(response.content))
25
 
26
  # Convert the image to the target format
27
+ output = BytesIO()
28
+ img.save(output, format=target_format)
29
+ output.seek(0)
30
+
31
+ # Convert to base64 string for JSON serialization
32
+ encoded_image = base64.b64encode(output.getvalue()).decode('utf-8')
33
+
34
+ return encoded_image # Return base64 encoded string that can be serialized to JSON
35
+ else:
36
+ img = Image.open(image)
37
+
38
+ output = BytesIO()
39
+ img.save(output, format=target_format)
40
+ output.seek(0)
41
+
42
+ # Convert to base64 string for JSON serialization
43
+ encoded_image = base64.b64encode(output.getvalue()).decode('utf-8')
44
+
45
+ return encoded_image
src/utils/resize_image.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from PIL import Image
2
+ from io import BytesIO
3
+ import requests
4
+ import base64
5
+ from typing import Union, Tuple
6
+
7
+ def resize_image(image_input: Union[str, BytesIO], target_size: Tuple[int, int], return_format: str = "base64") -> str:
8
+ """
9
+ Resize an image to the target size while maintaining aspect ratio.
10
+
11
+ Args:
12
+ image_input: URL, file path, base64 string, or BytesIO object
13
+ target_size: Tuple (width, height) for the target size
14
+ return_format: Format to return the image in ("base64" or "pil")
15
+
16
+ Returns:
17
+ Base64 encoded string of the resized image or PIL Image object
18
+ """
19
+ # Convert input to PIL Image
20
+ if isinstance(image_input, str):
21
+ if image_input.startswith(('http://', 'https://')):
22
+ # It's a URL
23
+ response = requests.get(image_input, timeout=10)
24
+ response.raise_for_status()
25
+ image = Image.open(BytesIO(response.content))
26
+ elif image_input.startswith('data:image'):
27
+ # It's a base64 data URI
28
+ base64_data = image_input.split(',')[1]
29
+ image = Image.open(BytesIO(base64.b64decode(base64_data)))
30
+ elif ';base64,' not in image_input and len(image_input) > 500:
31
+ # Likely a raw base64 string
32
+ image = Image.open(BytesIO(base64.b64decode(image_input)))
33
+ else:
34
+ # Assume it's a file path
35
+ image = Image.open(image_input)
36
+ elif isinstance(image_input, BytesIO):
37
+ image = Image.open(image_input)
38
+ else:
39
+ raise ValueError("Unsupported image input type")
40
+
41
+ # Calculate the aspect ratio
42
+ aspect_ratio = min(target_size[0] / image.width, target_size[1] / image.height)
43
+
44
+ # Calculate new size
45
+ new_size = (int(image.width * aspect_ratio), int(image.height * aspect_ratio))
46
+
47
+ # Resize the image using the proper resampling filter
48
+ resized_image = image.resize(new_size, Image.LANCZOS)
49
+
50
+ # Return in requested format
51
+ if return_format.lower() == "base64":
52
+ buffer = BytesIO()
53
+ resized_image.save(buffer, format="PNG")
54
+ return base64.b64encode(buffer.getvalue()).decode('utf-8')
55
+ else:
56
+ return resized_image