Skip to content

Commit 8142702

Browse files
committed
Accepting proposals
1 parent a4ebdde commit 8142702

2 files changed

Lines changed: 137 additions & 46 deletions

File tree

alerts.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,41 @@ def send_error_email(
5757
except Exception:
5858
# Nunca romper la app por alertas
5959
pass
60+
61+
def send_email(proposal: str):
62+
if not EMAIL_ALERTS_ENABLED:
63+
print("[EMAIL DESACTIVADO] Propuesta recibida:")
64+
print(proposal)
65+
return
66+
67+
try:
68+
msg = EmailMessage()
69+
msg["From"] = os.getenv("ALERT_EMAIL_FROM")
70+
msg["To"] = os.getenv("ALERT_EMAIL_TO")
71+
msg["Subject"] = "Nueva propuesta recibida en CV_Assistant"
72+
73+
body = f"""Fecha: {datetime.utcnow().isoformat()} UTC
74+
75+
Mensaje:
76+
{proposal}
77+
"""
78+
msg.set_content(body)
79+
80+
context_ssl = ssl.create_default_context()
81+
82+
with smtplib.SMTP(
83+
os.getenv("SMTP_HOST"),
84+
int(os.getenv("SMTP_PORT")),
85+
timeout=10
86+
) as server:
87+
server.starttls(context=context_ssl)
88+
server.login(
89+
os.getenv("SMTP_USER"),
90+
os.getenv("SMTP_PASSWORD")
91+
)
92+
server.send_message(msg)
93+
94+
except Exception:
95+
# Nunca romper la app por alertas
96+
print(f"Exception: {traceback.format_exc()}")
97+

main.py

Lines changed: 99 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
from collections import defaultdict
23

34
from dotenv import load_dotenv
@@ -56,27 +57,50 @@ def register_token_usage(ip: str, texts: list[str]):
5657
#print(resume)
5758

5859
name = "Miguel de la Ossa Abellán"
59-
system_prompt = (f"Estás actuando como {name}. Estás respondiendo a preguntas en el sitio web de {name}, " +
60-
"particularment preguntas relacionadas con la carrera, antecedentes, habilidades y experiencia de {name}. " +
61-
"Tu responsabilidad es representar a {name} para las interacciones en el sitio web de la manera más fiel posible. " +
62-
"Se te proporciona un resumen de los antecedentes de {name} y el perfil que puedes usar para responder preguntas. " +
63-
"Sé profesional y atractivo, como si hablaras con un cliente potencial o futuro empleador que se encontró con el sitio web. " +
64-
"Ten en cuenta que se han añadido proyectos en {resume} que no figuran en {profile}, que complementan {profile}." +
65-
"No contestes a preguntas no relacionadas con tu prefil. Simplemente, ignóralas. " +
66-
"Si te preguntan en inglés, contesta en inglés. Si lo hacen en español, contesta en español. " +
67-
"Si no sabes la respuesta, dilo. ")
68-
69-
system_prompt += f"\n\n## Resumen:\n{resume}\n\n## Perfil de LinedIn:\n{profile}\n\n"
70-
system_prompt += f"Con este contexto, por favor chatea con el usuario, manteniéndote siempre en el personaje de {name}."
60+
system_prompt = f"""Estás actuando como {name}. Estás respondiendo a preguntas en el sitio web de {name},
61+
particularment preguntas relacionadas con la carrera, antecedentes, habilidades y experiencia de {name}.
62+
Tu responsabilidad es representar a {name} para las interacciones en el sitio web de la manera más fiel posible.
63+
Se te proporciona un resumen de los antecedentes de {name} y el perfil que puedes usar para responder preguntas.
64+
Ten en cuenta que se han añadido proyectos en {resume} que no figuran en {profile}, que complementan {profile}.
65+
66+
INSTRUCCIONES IMPORTANTES:
67+
-Sé profesional y atractivo, como si hablaras con un cliente potencial o futuro empleador que se encontró con el sitio web.
68+
-No contestes a preguntas no relacionadas con tu prefil. Simplemente, ignóralas.
69+
-Si el usuario muestra interés en contactarte con una propuesta laboral, pide su email y regístralo usando
70+
la herramienta 'register_proposal', pero no preguntes por detalles adicionales. Simplemente, informa que registraste
71+
la propouesta.
72+
-Si te preguntan en inglés, contesta en inglés. Si lo hacen en español, contesta en español.
73+
-Si no sabes la respuesta, dilo.
74+
75+
## Resumen:
76+
{resume}
77+
78+
## Perfil de LinedIn:
79+
{profile}
80+
81+
Con este contexto, por favor chatea con el usuario, manteniéndote siempre en el personaje de {name}."""
7182

7283
#print(system_prompt)
7384

74-
def safe_openai_chat(model, messages, context=None):
85+
def safe_openai_chat(model, messages, tools, context=None):
7586
try:
76-
response = openai_client.chat.completions.create(
77-
model=model,
78-
messages=messages
79-
)
87+
finished = False
88+
response = {}
89+
while not finished:
90+
response = openai_client.chat.completions.create(
91+
model=model,
92+
messages=messages,
93+
tools=tools,
94+
)
95+
end_reason = response.choices[0].finish_reason
96+
if end_reason == "tool_calls":
97+
ia_message = response.choices[0].message
98+
tool_calls = ia_message.tool_calls
99+
results = manage_tools(tool_calls)
100+
messages.append(ia_message)
101+
messages.extend(results)
102+
else:
103+
finished = True
80104

81105
if not response.choices:
82106
raise ValueError("Respuesta sin choices")
@@ -117,6 +141,58 @@ def safe_perplexity_evaluate(messages):
117141
)
118142
raise
119143

144+
def register_proposal(email, name="No proporcionado", details="No proporcionados"):
145+
proposal = f"Contacto {name} con email {email} con propuesta: {details}"
146+
print(proposal)
147+
send_email(proposal)
148+
return {"register": "success"}
149+
150+
register_proposal_tool = {
151+
"name" : "register_proposal",
152+
"description": "Usa esta herramienta cuando un usuario quiera enviarme una propuesta y proporciona su email",
153+
"parameters": {
154+
"type": "object",
155+
"properties": {
156+
"email": {
157+
"type": "string",
158+
"description": "Dirección de email del usuario"
159+
},
160+
"name": {
161+
"type": "string",
162+
"description": "Nombre del usuario, si lo proporcionó"
163+
},
164+
"details": {
165+
"type": "string",
166+
"description": "Detalles de la propuesta, si los proporcionó"
167+
}
168+
},
169+
"required": ["email"],
170+
"additionalProperties": False
171+
}
172+
}
173+
174+
registered_tools = [
175+
{"type": "function", "function": register_proposal_tool}
176+
]
177+
178+
def manage_tools(tool_calls):
179+
results = []
180+
for call in tool_calls:
181+
tool_name = call.function.name
182+
args = json.loads(call.function.arguments)
183+
print(f"Ejecutando herramienta {tool_name}", flush=True)
184+
result = {}
185+
if tool_name == "register_proposal":
186+
result = register_proposal(**args)
187+
else:
188+
result = {"error": "Herramienta no existe"}
189+
results.append({
190+
"role": "tool",
191+
"content": json.dumps(result),
192+
"tool_call_id": call.id
193+
})
194+
return results
195+
120196
# def safe_gemini_evaluate(messages):
121197
# try:
122198
# response = gemini.beta.chat.completions.parse(
@@ -162,11 +238,13 @@ class Evaluation(BaseModel):
162238
"o futuro empleador que se encontró con el sitio web. " + \
163239
"Pero debes asegurarte de que no inventa nada que no esté en su resumen ni en los detalles de LinkedIn; " + \
164240
"controla que el agente no cree detalles inexistentes. " + \
165-
"Tampoco aceptes ninguna propuesta de trabajo. En ese caso, invita al posible empleador a enviar un " + \
166-
"email a la dirección que figura en el perfil de LinkedIn o en el currículum. " + \
241+
"Si el agente acepta una propuesta, acepta la respuesta de forma incondicional. " + \
167242
"Se ha proporcionado al agente el contexto sobre {name} en forma de su resumen y detalles en LinkedIn. " + \
168243
"Aquí está la información:"
169244

245+
# "Tampoco aceptes ninguna propuesta de trabajo. En ese caso, invita al posible empleador a enviar un " + \
246+
# "email a la dirección que figura en el perfil de LinkedIn o en el currículum. " + \
247+
170248
prompt_evaluation_system += f"\n\n## Resumen:\n{resume}\n\n## Perfil de LinedIn:\n{profile}\n\n"
171249
prompt_evaluation_system += f"Con este contexto, por favor evalúa la última respuesta, respondiendo si la respuesta " + \
172250
"es aceptable y tu retroalimentación."
@@ -204,32 +282,6 @@ def rexecute(answer, message, history, retroalimentation):
204282
new_answer = openai_client.chat.completions.create(model="o4-mini", messages=messages)
205283
return new_answer.choices[0].message.content
206284

207-
# def chatting(message, history):
208-
# #system = system_prompt
209-
# # Si es la primera vez (no hay historia), ignoramos `message` y pedimos solo el saludo
210-
# if not history:
211-
# messages = [{"role": "system", "content": system_prompt}]
212-
# answer = openai_client.chat.completions.create(
213-
# model="o4-mini",
214-
# messages=messages
215-
# )
216-
# chat_response = answer.choices[0].message.content
217-
# return chat_response
218-
#
219-
# messages = [{"role": "system", "content": system_prompt}] + history + [{"role": "user", "content": message}]
220-
# answer = openai_client.chat.completions.create(model="o4-mini", messages=messages)
221-
# chat_response = answer.choices[0].message.content
222-
#
223-
# evaluation = evaluate(chat_response, message, history)
224-
#
225-
# if evaluation.is_acceptable:
226-
# print("Pasó la evaluación - devolviendo respuesta")
227-
# else:
228-
# print("Falló la evaluación - reintentando")
229-
# print(evaluation.retroalimentation)
230-
# chat_response = rexecute(chat_response, message, history, evaluation.retroalimentation)
231-
# return chat_response
232-
233285
def chatting(message, history, request: gr.Request | None = None):
234286
ip = "unknown"
235287

@@ -261,16 +313,17 @@ def chatting(message, history, request: gr.Request | None = None):
261313
return safe_openai_chat(
262314
model="o4-mini",
263315
messages=[{"role": "system", "content": system_prompt}],
316+
tools=registered_tools,
264317
context={"phase": "intro"}
265318
)
266319

267320
messages = [{"role": "system", "content": system_prompt}] + history + [
268321
{"role": "user", "content": message}
269322
]
270-
271323
chat_response = safe_openai_chat(
272324
model="o4-mini",
273325
messages=messages,
326+
tools=registered_tools,
274327
context={"phase": "answer"}
275328
)
276329

0 commit comments

Comments
 (0)