Mariusz47 commited on
Commit
f3c5f38
·
1 Parent(s): 57f5f94

pushe all project files

Browse files
.env.example ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ OPENAI_API_KEY=""
2
+ GROQ_API_KEY=""
3
+ ANTHROPIC_API_KEY=""
4
+ GOOGLE_API_KEY=""
5
+ HF_TOKEN=""
6
+ LANGSMITH_API_KEY=""
7
+
8
+ LANGSMITH_ENDPOINT="https://api.smith.langchain.com"
9
+ LANGSMITH_TRACING_V2=true
10
+ LANGCHAIN_PROJECT=""
11
+ PROFIL_NAME=""
.gitignore CHANGED
@@ -205,3 +205,9 @@ cython_debug/
205
  marimo/_static/
206
  marimo/_lsp/
207
  __marimo__/
 
 
 
 
 
 
 
205
  marimo/_static/
206
  marimo/_lsp/
207
  __marimo__/
208
+
209
+ # Mac
210
+ .DS_Store
211
+
212
+ # My folders
213
+ /me/*.txt
README.md CHANGED
@@ -1,2 +1,30 @@
1
- # profile-avatar-chat
2
- An interactive personal profile website where visitors can chat with an AI avatar to learn about my background and experience.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Your App Name
3
+ emoji: 🤖
4
+ sdk: gradio
5
+ app_file: src/app.py
6
+ pinned: false
7
+ ---
8
+
9
+
10
+ # Profile Avatar Chat App
11
+
12
+ This repository contains the code for a robust AI-powered chat service that acts as a personal profile avatar. The chat responds based on my LinkedIn profile, professional summary, current situation, recommendations, and other additional information.
13
+
14
+ Key features implemented for robustness:
15
+
16
+ - Semantic QA cache: Reuses previous answers for repeated or similar questions to improve response speed and consistency.
17
+
18
+ - Embedding-based similarity search: Uses OpenAI embeddings and cosine similarity to find semantically similar past questions and refine answers.
19
+
20
+ - Sliding window conversation context: Keeps only the last n messages for token-efficient API calls while preserving relevant context.
21
+
22
+ - Automated evaluation and rerun: Uses Google Gemini (via OpenAI API wrapper) to evaluate generated responses, automatically rerunning and refining answers when quality control flags them.
23
+
24
+ - Traceability with LangSmith: Key functions are decorated for run tracking, enabling debugging and historical inspection of chat interactions.
25
+
26
+ - PDF and text ingestion: Extracts profile information from LinkedIn PDF, summary, current situation, and recommendation text files.
27
+
28
+ - Gradio integration: Provides an interactive chat interface for local testing and deployment.
29
+
30
+ This chat service powers my portfolio website, which communicates with this deployed Hugging Face Space for live interactions.
me/Linkedin_Profile.pdf ADDED
Binary file (65.2 kB). View file
 
notebooks/chat_with_avatar.ipynb ADDED
@@ -0,0 +1,378 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "3471e6b1",
6
+ "metadata": {},
7
+ "source": [
8
+ "# Chat With Avatar About My Experience and Skills"
9
+ ]
10
+ },
11
+ {
12
+ "cell_type": "code",
13
+ "execution_count": null,
14
+ "id": "5dcb5ef0",
15
+ "metadata": {},
16
+ "outputs": [],
17
+ "source": [
18
+ "import os\n",
19
+ "from dotenv import load_dotenv\n",
20
+ "from openai import OpenAI\n",
21
+ "from pypdf import PdfReader\n",
22
+ "import gradio as gr"
23
+ ]
24
+ },
25
+ {
26
+ "cell_type": "code",
27
+ "execution_count": null,
28
+ "id": "f5176f5c",
29
+ "metadata": {},
30
+ "outputs": [],
31
+ "source": [
32
+ "load_dotenv(override=True)\n",
33
+ "\n",
34
+ "openai_api_key = os.getenv(\"OPENAI_API_KEY\")\n",
35
+ "google_api_key = os.getenv(\"GOOGLE_API_KEY\")\n",
36
+ "groq_api_key = os.getenv(\"GROQ_API_KEY\")\n"
37
+ ]
38
+ },
39
+ {
40
+ "cell_type": "code",
41
+ "execution_count": null,
42
+ "id": "be1a140b",
43
+ "metadata": {},
44
+ "outputs": [],
45
+ "source": [
46
+ "openai = OpenAI()\n",
47
+ "gemini = OpenAI(api_key=google_api_key, base_url=\"https://generativelanguage.googleapis.com/v1beta/openai/\")"
48
+ ]
49
+ },
50
+ {
51
+ "cell_type": "code",
52
+ "execution_count": null,
53
+ "id": "da87405b",
54
+ "metadata": {},
55
+ "outputs": [],
56
+ "source": [
57
+ "reader = PdfReader(\"../me/Linkedin_Profile.pdf\")\n",
58
+ "linkedin = \"\"\n",
59
+ "for page in reader.pages:\n",
60
+ " text = page.extract_text()\n",
61
+ " if text:\n",
62
+ " linkedin += text"
63
+ ]
64
+ },
65
+ {
66
+ "cell_type": "code",
67
+ "execution_count": null,
68
+ "id": "386847b5",
69
+ "metadata": {},
70
+ "outputs": [],
71
+ "source": [
72
+ "# print(linkedin)"
73
+ ]
74
+ },
75
+ {
76
+ "cell_type": "code",
77
+ "execution_count": null,
78
+ "id": "7ae1fd8d",
79
+ "metadata": {},
80
+ "outputs": [],
81
+ "source": [
82
+ "with open(\"../me/summary.txt\", \"r\", encoding=\"utf-8\") as f:\n",
83
+ " summary = f.read()"
84
+ ]
85
+ },
86
+ {
87
+ "cell_type": "code",
88
+ "execution_count": null,
89
+ "id": "c08f3db9",
90
+ "metadata": {},
91
+ "outputs": [],
92
+ "source": [
93
+ "with open(\"../me/current_situation.txt\", \"r\", encoding=\"utf-8\") as f:\n",
94
+ " current_situation = f.read()"
95
+ ]
96
+ },
97
+ {
98
+ "cell_type": "code",
99
+ "execution_count": null,
100
+ "id": "fac6fde8",
101
+ "metadata": {},
102
+ "outputs": [],
103
+ "source": [
104
+ "name = \"Mariusz Bronowicki\""
105
+ ]
106
+ },
107
+ {
108
+ "cell_type": "code",
109
+ "execution_count": null,
110
+ "id": "3a20a2b4",
111
+ "metadata": {},
112
+ "outputs": [],
113
+ "source": [
114
+ "system_prompt = f\"You are acting as {name}. You are answering question on {name}'s website, \\\n",
115
+ "particularly question related to {name}'s career, background, skills and experience. \\\n",
116
+ "Your responsibility is to represent {name} for interactions on the website as faithfully as possible. \\\n",
117
+ "Be professional and engaging, as if talking to a potential client or future employer who came across the website. \\\n",
118
+ "If you do not know the answer, say so. \\\n",
119
+ "If you need to check e.g salary expectation question then use tools to see what range for such position is.\"\n",
120
+ "\n",
121
+ "system_prompt += f\"\\n\\n## Summary:\\n{summary}\\n\\n## Linkedin Profile:\\n{linkedin}\\n\\n## Current situation:\\n{current_situation}\\n\\n\"\n",
122
+ "system_prompt += f\"With this context, please chat with user, always staying in character as {name}.\""
123
+ ]
124
+ },
125
+ {
126
+ "cell_type": "code",
127
+ "execution_count": null,
128
+ "id": "61832d91",
129
+ "metadata": {},
130
+ "outputs": [],
131
+ "source": [
132
+ "system_prompt"
133
+ ]
134
+ },
135
+ {
136
+ "cell_type": "code",
137
+ "execution_count": null,
138
+ "id": "b1421ebf",
139
+ "metadata": {},
140
+ "outputs": [],
141
+ "source": [
142
+ "def chat_gpt(message, history):\n",
143
+ " messages = [{\"role\": \"user\", \"content\": system_prompt}] + history + [{\"role\": \"user\", \"content\": message}]\n",
144
+ " response = openai.chat.completions.create(model=\"gpt-4o-mini\", messages=messages)\n",
145
+ " return response.choices[0].message.content"
146
+ ]
147
+ },
148
+ {
149
+ "cell_type": "code",
150
+ "execution_count": null,
151
+ "id": "b7c32734",
152
+ "metadata": {},
153
+ "outputs": [],
154
+ "source": []
155
+ },
156
+ {
157
+ "cell_type": "code",
158
+ "execution_count": null,
159
+ "id": "1a96fabc",
160
+ "metadata": {},
161
+ "outputs": [],
162
+ "source": [
163
+ "def chat_gemini(message, history):\n",
164
+ " history = [{\"role\": h[\"role\"], \"content\": h[\"content\"]} for h in history]\n",
165
+ " messages = [{\"role\": \"user\", \"content\": system_prompt}] + history + [{\"role\": \"user\", \"content\": message}]\n",
166
+ " response = gemini.chat.completions.create(model=\"gemini-2.0-flash\", messages=messages)\n",
167
+ " return response.choices[0].message.content"
168
+ ]
169
+ },
170
+ {
171
+ "cell_type": "code",
172
+ "execution_count": null,
173
+ "id": "44aa35da",
174
+ "metadata": {},
175
+ "outputs": [],
176
+ "source": [
177
+ "gr.ChatInterface(chat_gpt, type=\"messages\").launch()"
178
+ ]
179
+ },
180
+ {
181
+ "cell_type": "code",
182
+ "execution_count": null,
183
+ "id": "d43f04f7",
184
+ "metadata": {},
185
+ "outputs": [],
186
+ "source": [
187
+ "gr.ChatInterface(chat_gemini, type=\"messages\").launch()"
188
+ ]
189
+ },
190
+ {
191
+ "cell_type": "markdown",
192
+ "id": "4a5ab195",
193
+ "metadata": {},
194
+ "source": [
195
+ "## Ask LLM to evaluate answer from previous model.\n",
196
+ "\n",
197
+ "All without any Agentic Framework!"
198
+ ]
199
+ },
200
+ {
201
+ "cell_type": "code",
202
+ "execution_count": null,
203
+ "id": "8e1c26d8",
204
+ "metadata": {},
205
+ "outputs": [],
206
+ "source": [
207
+ "# Create a Pydantic model for the Evaluation\n",
208
+ "from pydantic import BaseModel\n",
209
+ "\n",
210
+ "class Evaluation(BaseModel):\n",
211
+ " is_acceptable: bool\n",
212
+ " feedback: str"
213
+ ]
214
+ },
215
+ {
216
+ "cell_type": "code",
217
+ "execution_count": null,
218
+ "id": "bfd6a08d",
219
+ "metadata": {},
220
+ "outputs": [],
221
+ "source": [
222
+ "evaluator_system_prompt = f\"You are an evaluator that decides whether a response to a question is acceeptable. \\\n",
223
+ "You are provided with a conversation btween a User and an Agent. Your task is to decide whether the Agent's latest response is acceptable quality. \\\n",
224
+ "The Agent is playing the role of {name} and is representing {name} on their website. \\\n",
225
+ "The Agent has been instructed to be professional and engaging, as if talking to a potential client or future employer who came across the website. \\\n",
226
+ "The Agent has been provided with context on {name} in the form of their summary and Linkedin details. Here's the information:\"\n",
227
+ "\n",
228
+ "evaluator_system_prompt += f\"\\n\\n## Summary:\\n{summary}\\n\\n## Linkedin Profile{linkedin}\\n\\n\"\n",
229
+ "evaluator_system_prompt += f\"With this context, please evaluate the latest response, replying with whether the response is acceptable and your feedback.\""
230
+ ]
231
+ },
232
+ {
233
+ "cell_type": "code",
234
+ "execution_count": null,
235
+ "id": "aaada426",
236
+ "metadata": {},
237
+ "outputs": [],
238
+ "source": [
239
+ "def evaluator_user_prompt(reply, message, history):\n",
240
+ " user_prompt = f\"Here's the conversation between the User and the Agent: \\n\\n{history}\\n\\n\"\n",
241
+ " user_prompt += f\"Here's the latest message from the User: \\n\\n{message}\\n\\n\"\n",
242
+ " user_prompt += f\"Here's the latest response from the Agent: \\n\\n{reply}\\n\\n\"\n",
243
+ " user_prompt += f\"Please evaluate the response, replying with whether it is acceptable and your feedback.\"\n",
244
+ " return user_prompt"
245
+ ]
246
+ },
247
+ {
248
+ "cell_type": "code",
249
+ "execution_count": null,
250
+ "id": "522a926c",
251
+ "metadata": {},
252
+ "outputs": [],
253
+ "source": [
254
+ "def evaluate(reply, message, history) -> Evaluation:\n",
255
+ " messages = [{\"role\": \"system\", \"content\": evaluator_system_prompt}] + [{\"role\": \"user\", \"content\": evaluator_user_prompt(reply, message, history)}]\n",
256
+ " response = gemini.chat.completions.parse(model=\"gemini-2.0-flash\", messages=messages, response_format=Evaluation)\n",
257
+ " return response.choices[0].message.parsed"
258
+ ]
259
+ },
260
+ {
261
+ "cell_type": "code",
262
+ "execution_count": null,
263
+ "id": "631098e3",
264
+ "metadata": {},
265
+ "outputs": [],
266
+ "source": [
267
+ "messages = [{\"role\": \"system\", \"content\": system_prompt}] + [{\"role\": \"user\", \"content\": \"What is your current situation?\"}]\n",
268
+ "response = openai.chat.completions.create(model=\"gpt-4o-mini\", messages=messages)\n",
269
+ "reply = response.choices[0].message.content"
270
+ ]
271
+ },
272
+ {
273
+ "cell_type": "code",
274
+ "execution_count": null,
275
+ "id": "e0338b90",
276
+ "metadata": {},
277
+ "outputs": [],
278
+ "source": [
279
+ "reply"
280
+ ]
281
+ },
282
+ {
283
+ "cell_type": "code",
284
+ "execution_count": null,
285
+ "id": "7f271a3a",
286
+ "metadata": {},
287
+ "outputs": [],
288
+ "source": [
289
+ "evaluate(reply, \"What is your current situation?\", messages[:1])"
290
+ ]
291
+ },
292
+ {
293
+ "cell_type": "code",
294
+ "execution_count": null,
295
+ "id": "84923137",
296
+ "metadata": {},
297
+ "outputs": [],
298
+ "source": [
299
+ "def rerun(reply, message, history,feedback):\n",
300
+ " updated_system_prompt = system_prompt + \"\\n\\n## Previous answer rejected\\n \\\n",
301
+ " You just tried to reply, but the quality control rejected your reply\\n\"\n",
302
+ " updated_system_prompt += f\"## Your attempted answer:\\n{reply}\\n\\n\"\n",
303
+ " updated_system_prompt += f\"## Reason for rejection:\\n{feedback}\\n\\n\"\n",
304
+ " messages = [{\"role\": \"system\", \"content\": updated_system_prompt}] + history + [{\"role\": \"user\", \"content\": message}]\n",
305
+ " response = openai.chat.completions.create(model=\"gpt-4o-mini\", messages=messages)\n",
306
+ " return response.choices[0].message.content"
307
+ ]
308
+ },
309
+ {
310
+ "cell_type": "code",
311
+ "execution_count": null,
312
+ "id": "943dc4d6",
313
+ "metadata": {},
314
+ "outputs": [],
315
+ "source": [
316
+ "def chat(message, history):\n",
317
+ " # if \"tell me about yourself\" in message:\n",
318
+ " # system = system_prompt + \"\\n\\nEverything in you reply needs to be in pig latin - \\\n",
319
+ " # it is mandatory that you response only and entirely in pig latin\"\n",
320
+ " # else:\n",
321
+ " # system = system_prompt\n",
322
+ " system = system_prompt\n",
323
+ " messages = [{\"role\": \"system\", \"content\": system}] + history + [{\"role\": \"user\", \"content\": message}]\n",
324
+ " response = openai.chat.completions.create(model=\"gpt-4o-mini\", messages=messages)\n",
325
+ " reply = response.choices[0].message.content\n",
326
+ "\n",
327
+ " evaluation = evaluate(reply, message, history)\n",
328
+ "\n",
329
+ " if evaluation.is_acceptable:\n",
330
+ " print(\"Passed evaluation - returning reply\")\n",
331
+ " else:\n",
332
+ " print(\"Failed evaluation - retrying\")\n",
333
+ " print(evaluation.feedback)\n",
334
+ " reply = rerun(reply, message, history, evaluation.feedback)\n",
335
+ " return reply"
336
+ ]
337
+ },
338
+ {
339
+ "cell_type": "code",
340
+ "execution_count": null,
341
+ "id": "c74ee145",
342
+ "metadata": {},
343
+ "outputs": [],
344
+ "source": [
345
+ "gr.ChatInterface(chat, type=\"messages\").launch()"
346
+ ]
347
+ },
348
+ {
349
+ "cell_type": "code",
350
+ "execution_count": null,
351
+ "id": "36cbe706",
352
+ "metadata": {},
353
+ "outputs": [],
354
+ "source": []
355
+ }
356
+ ],
357
+ "metadata": {
358
+ "kernelspec": {
359
+ "display_name": "profile-avatar-chat",
360
+ "language": "python",
361
+ "name": "python3"
362
+ },
363
+ "language_info": {
364
+ "codemirror_mode": {
365
+ "name": "ipython",
366
+ "version": 3
367
+ },
368
+ "file_extension": ".py",
369
+ "mimetype": "text/x-python",
370
+ "name": "python",
371
+ "nbconvert_exporter": "python",
372
+ "pygments_lexer": "ipython3",
373
+ "version": "3.12.11"
374
+ }
375
+ },
376
+ "nbformat": 4,
377
+ "nbformat_minor": 5
378
+ }
pyproject.toml ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "profile-avatar-chat"
3
+ version = "0.1.0"
4
+ description = "Add your description"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = [
8
+ "dotenv>=0.9.9",
9
+ "anthropic>=0.49.0",
10
+ "autogen-agentchat>=0.4.9.2",
11
+ "autogen-ext[grpc,mcp,ollama,openai]>=0.4.9.2",
12
+ "bs4>=0.0.2",
13
+ "gradio>=5.22.0",
14
+ "httpx>=0.28.1",
15
+ "ipywidgets>=8.1.5",
16
+ "langchain-anthropic>=0.3.10",
17
+ "langchain-community>=0.3.20",
18
+ "langchain-experimental>=0.3.4",
19
+ "langchain-openai>=0.3.9",
20
+ "langgraph>=0.3.18",
21
+ "langgraph-checkpoint-sqlite>=2.0.6",
22
+ "langsmith>=0.3.18",
23
+ "lxml>=5.3.1",
24
+ "mcp-server-fetch>=2025.1.17",
25
+ "mcp[cli]>=1.5.0",
26
+ "openai>=1.68.2",
27
+ "openai-agents>=0.0.15",
28
+ "playwright>=1.51.0",
29
+ # "plotly>=6.0.1",
30
+ "polygon-api-client>=1.14.5",
31
+ "psutil>=7.0.0",
32
+ "pypdf>=5.4.0",
33
+ "pypdf2>=3.0.1",
34
+ "python-dotenv>=1.0.1",
35
+ "requests>=2.32.3",
36
+ "semantic-kernel>=1.25.0",
37
+ "sendgrid>=6.11.0",
38
+ "setuptools>=78.1.0",
39
+ "smithery>=0.1.0",
40
+ "speedtest-cli>=2.1.3",
41
+ "scikit-learn>=1.7.2",
42
+ #"wikipedia>=1.4.0",
43
+ ]
44
+
45
+ [dependency-groups]
46
+ dev = [
47
+ "ipykernel>=6.29.5",
48
+ ]
requirements.txt ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "dotenv>=0.9.9",
2
+ "anthropic>=0.49.0",
3
+ "autogen-agentchat>=0.4.9.2",
4
+ "autogen-ext[grpc,mcp,ollama,openai]>=0.4.9.2",
5
+ "bs4>=0.0.2",
6
+ "gradio>=5.22.0",
7
+ "httpx>=0.28.1",
8
+ "ipywidgets>=8.1.5",
9
+ "langchain-anthropic>=0.3.10",
10
+ "langchain-community>=0.3.20",
11
+ "langchain-experimental>=0.3.4",
12
+ "langchain-openai>=0.3.9",
13
+ "langgraph>=0.3.18",
14
+ "langgraph-checkpoint-sqlite>=2.0.6",
15
+ "langsmith>=0.3.18",
16
+ "lxml>=5.3.1",
17
+ "mcp-server-fetch>=2025.1.17",
18
+ "mcp[cli]>=1.5.0",
19
+ "openai>=1.68.2",
20
+ "openai-agents>=0.0.15",
21
+ "playwright>=1.51.0",
22
+ "polygon-api-client>=1.14.5",
23
+ "psutil>=7.0.0",
24
+ "pypdf>=5.4.0",
25
+ "pypdf2>=3.0.1",
26
+ "python-dotenv>=1.0.1",
27
+ "requests>=2.32.3",
28
+ "semantic-kernel>=1.25.0",
29
+ "sendgrid>=6.11.0",
30
+ "setuptools>=78.1.0",
31
+ "smithery>=0.1.0",
32
+ "speedtest-cli>=2.1.3",
33
+ "scikit-learn>=1.7.2",
src/__init__.py ADDED
File without changes
src/app.py ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+ from pydantic import BaseModel
4
+ from openai import OpenAI
5
+ from pypdf import PdfReader
6
+ import gradio as gr
7
+ from prompts import system_prompt, evaluator_system_prompt
8
+ from langsmith import Client, traceable
9
+ from sklearn.metrics.pairwise import cosine_similarity
10
+ import traceback
11
+
12
+ import numpy as np
13
+
14
+ class Evaluation(BaseModel):
15
+ is_acceptable: bool
16
+ feedback: str
17
+
18
+ class Config:
19
+ def __init__(self):
20
+ load_dotenv(override=True)
21
+ self.openai_api_key = os.getenv("OPENAI_API_KEY")
22
+ self.google_api_key = os.getenv("GOOGLE_API_KEY")
23
+ self.langsmith_api_key = os.getenv("LANGSMITH_API_KEY")
24
+ self.langsmith_endpoint = os.getenv("LANGSMITH_ENDPOINT")
25
+
26
+ # Initialize LangSmith
27
+ self.langsmith_client = Client(api_key=self.langsmith_api_key)
28
+
29
+ # print(f"OpenAI Api Key: {self.openai_api_key[:7]}")
30
+
31
+ class FileReader:
32
+ def __init__(self):
33
+ self.linkedin_profile = ""
34
+ try:
35
+ reader = PdfReader("../me/Linkedin_Profile.pdf")
36
+ for page in reader.pages:
37
+ text = page.extract_text()
38
+ if text:
39
+ self.linkedin_profile += text
40
+ except Exception:
41
+ # If file missing, keep empty
42
+ self.linkedin_profile = ""
43
+ # NOT IMPLEMENTED ---> CREATE FILE AND CHANGE IN THE APP WHERE APPLICABLE
44
+ try:
45
+ with open("../me/additional_info.txt", "r", encoding="utf-8") as f:
46
+ self.additional_info = f.read()
47
+ except:
48
+ self.additional_info = ""
49
+
50
+
51
+ class MyProfileAvatarChat(Config, FileReader):
52
+ def __init__(self, max_history_turns: int = 10, similarity_thresh: float = 0.80):
53
+ Config.__init__(self)
54
+ FileReader.__init__(self)
55
+
56
+ self.name = os.getenv("PROFIL_NAME")
57
+ self.openai = OpenAI(api_key=self.openai_api_key)
58
+ # gemini (evaluator) uses google_api_key via OpenAI wrapper
59
+ self.gemini = OpenAI(api_key=self.google_api_key,
60
+ base_url="https://generativelanguage.googleapis.com/v1beta/openai/")
61
+
62
+ # Build system prompt once
63
+ self.system_prompt = system_prompt
64
+ self.system_prompt += f"## Linkedin Profile:\n{self.linkedin_profile}\n\n"
65
+ self.system_prompt += f"## Addidional Information:\n{self.additional_info}\n\n"
66
+ self.system_prompt += f"With this context, please chat with user, always staying in character as {self.name}."
67
+
68
+ self.evaluator_system_prompt = evaluator_system_prompt
69
+
70
+ # Settings
71
+ self.max_history_turns = max_history_turns
72
+ self.similarity_threshold = similarity_thresh
73
+
74
+ # QA cache (question -> answer -> embedding)
75
+ self.qa_cache = [] # list of dict: {"question": str, "answer": str, "embedding": np.array}
76
+
77
+
78
+ def format_history(self, history):
79
+ return "\n".join(f"{turn['role'].upper()}: {turn['content']}" for turn in history)
80
+
81
+ def embed(self, text: str):
82
+ """Return embedding vector for text (uses OpenAI embeddings)."""
83
+ resp = self.openai.embeddings.create(
84
+ model="text-embedding-3-small",
85
+ input=text
86
+ )
87
+ return np.array(resp.data[0].embedding)
88
+
89
+ def cosine_sim(self, a: np.ndarray, b: np.ndarray) -> float:
90
+ return float(cosine_similarity(a.reshape(1, -1), b.reshape(1, -1))[0][0])
91
+
92
+ def find_similar_question(self, new_question: str):
93
+ if not self.qa_cache:
94
+ return None, 0.0
95
+ new_emb = self.embed(new_question)
96
+ best = None
97
+ best_sim = 0.0
98
+ for item in self.qa_cache:
99
+ sim = self.cosine_sim(new_emb, item["embedding"])
100
+ if sim > best_sim:
101
+ best_sim = sim
102
+ best = item
103
+ if best and best_sim >= self.similarity_threshold:
104
+ return best, best_sim
105
+ return None, best_sim
106
+
107
+ def evaluator_user_prompt(self, reply, message, history):
108
+ formatted_history = self.format_history(history)
109
+ user_prompt = f"Here's the conversation between the User and the Agent: \n\n{formatted_history}\n\n"
110
+ user_prompt += f"Here's the latest message from the User: \n\n{message}\n\n"
111
+ user_prompt += f"Here's the latest response from the Agent: \n\n{reply}\n\n"
112
+ user_prompt += f"Please evaluate the response, replying with whether it is acceptable and your feedback."
113
+ return user_prompt
114
+
115
+ @traceable(run_type="tool", name="EvaluateReply")
116
+ def evaluate(self, reply, message, history, **kwargs) -> Evaluation:
117
+ messages = [{"role": "system", "content": self.evaluator_system_prompt}] + \
118
+ [{"role": "user", "content": self.evaluator_user_prompt(reply, message, history)}]
119
+ response = self.gemini.chat.completions.parse(
120
+ model="gemini-2.0-flash",
121
+ messages=messages,
122
+ response_format=Evaluation
123
+ )
124
+ return response.choices[0].message.parsed
125
+
126
+ @traceable(run_type="llm", name="RerunRejectedAnswer")
127
+ def rerun(self, reply, message, history, feedback, **kwargs):
128
+ # updated_system_prompt = self.system_prompt + "\n\n## Previous answer rejected\n \
129
+ # You just tried to reply, but the quality control rejected your reply\n"
130
+ # updated_system_prompt += f"## Your attempted answer:\n{reply}\n\n"
131
+ # updated_system_prompt += f"## Reason for rejection:\n{feedback}\n\n"
132
+
133
+ updated_system_prompt = (
134
+ self.system_prompt
135
+ + "\n\n## Previous answer rejected\n"
136
+ + "You just tried to reply, but the quality control rejected your reply\n"
137
+ + f"## Your attempted answer:\n{reply}\n\n"
138
+ + f"## Reason for rejection:\n{feedback}\n\n"
139
+ )
140
+
141
+
142
+ messages = [{"role": "system", "content": updated_system_prompt}] + history + \
143
+ [{"role": "user", "content": message}]
144
+ try:
145
+ response = self.openai.chat.completions.create(
146
+ model="gpt-4o-mini",
147
+ messages=messages
148
+ )
149
+ return response.choices[0].message.content
150
+ except Exception as e:
151
+ print(f"Error during rerun: {e}")
152
+ return reply
153
+
154
+ def chat(self, message: str, history: list, **kwargs):
155
+ """Main chat. Uses semantic QA cache, sliding window for tokens, evaluation and rerun
156
+
157
+ Args:
158
+ message: user message string
159
+ history: existing list of dicts [{"role":...., "content":....}]
160
+ Returns:
161
+ reply string
162
+ """
163
+
164
+ # Cache exact-match short-circuit
165
+ if message in (qa["question"] for qa in self.qa_cache):
166
+ # exact match
167
+ for qa in self.qa_cache:
168
+ if qa["question"] == message:
169
+ print("Using exact cached reply")
170
+ history.append({"role": "user", "content": message})
171
+ history.append({"role": "assistant", "content": qa["answer"]})
172
+ return qa["answer"]
173
+
174
+ # Check for semantically similar previous question
175
+ similar, sim_score = self.find_similar_question(message)
176
+ if similar:
177
+ print(f"Reusing past answer (similarity={sim_score:.2%})")
178
+ refine_prompt = (
179
+ f"The user previously asked a similar question:\n"
180
+ + f"Old question: {similar['question']}\n"
181
+ + f"Old answer: {similar['answer']}\n\n"
182
+ + f"Now user asks: {message}\n\n"
183
+ + f"Please update or refine the old answer to match the new question."
184
+ )
185
+ messages = [{"role": "system", "content": self.system_prompt},
186
+ {"role": "user", "content": refine_prompt}]
187
+
188
+ try:
189
+ response = self.openai.chat.completions.create(
190
+ model="gpt-4o-mini",
191
+ messages=messages
192
+ )
193
+ reply = response.choices[0].message.content
194
+ except Exception as e:
195
+ print(f"Error calling OpenAI for refinement: {e}")
196
+ reply = similar["answer"]
197
+
198
+ else:
199
+ # Build token-efficent context (sliding window)
200
+ temp_history = history + [{"role": "user", "content": message}]
201
+ context_for_api = temp_history[-self.max_history_turns:]
202
+ messages = [{"role": "system", "content": self.system_prompt}] + context_for_api
203
+
204
+ try:
205
+ response = self.openai.chat.completions.create(
206
+ model="gpt-4o-mini",
207
+ messages=messages
208
+ )
209
+ reply = response.choices[0].message.content
210
+ except Exception as e:
211
+ print(f"Error calling OpenAI: {e}")
212
+
213
+ # Evaluate the reply
214
+ try:
215
+ evaluation = self.evaluate(reply, message, history)
216
+ except Exception as e:
217
+ print(f"Error during evaluation: {e}")
218
+ evaluation = Evaluation(is_acceptable=True, feedback="Evaluation failed, accepting reply")
219
+
220
+ if not evaluation.is_acceptable:
221
+ reply = self.rerun(reply, message, history, evaluation.feedback)
222
+
223
+ try:
224
+ emb = self.embed(message)
225
+ except Exception as e:
226
+ print(f"Embedding Error: {e}")
227
+ traceback.print_exc()
228
+ emb = None
229
+
230
+ self.qa_cache.append({
231
+ "question": message,
232
+ "answer": reply,
233
+ "embedding": emb
234
+ })
235
+
236
+ return reply
237
+
238
+ @traceable(run_type="chain", name="ProfileChat")
239
+ def chat_traced(self, *args, **kwargs):
240
+ """Wrapper for LangSmith tracing. Accepts any extra arguments
241
+ (like from Gradio) and passes only message/history to chat()."""
242
+
243
+ if len(args) >=2:
244
+ message, history = args[0], args[1]
245
+ else:
246
+ message = kwargs.get("message")
247
+ history = kwargs.get("history")
248
+ return self.chat(message, history)
249
+
250
+ if __name__ == "__main__":
251
+
252
+ my_profile = MyProfileAvatarChat()
253
+ gr.ChatInterface(my_profile.chat_traced, type="messages").launch()
254
+
src/prompts.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+
4
+ load_dotenv(override=True)
5
+
6
+ name = os.getenv("PROFIL_NAME")
7
+
8
+ system_prompt = f"You are acting as {name}. You are answering question on {name}'s website, \
9
+ particularly question related to {name}'s career, background, skills and experience. \
10
+ Your responsibility is to represent {name} for interactions on the website as faithfully as possible. \
11
+ Be professional and engaging, as if talking to a potential client or future employer who came across the website. \
12
+ If you do not know the answer, say so. \
13
+ If you need to check e.g salary expectation question then use tools to see what range for such position is."
14
+
15
+ evaluator_system_prompt = f"You are an evaluator that decides whether a response to a question is acceeptable. \
16
+ You are provided with a conversation btween a User and an Agent. Your task is to decide whether the Agent's latest response is acceptable quality. \
17
+ The Agent is playing the role of {name} and is representing {name} on their website. \
18
+ The Agent has been instructed to be professional and engaging, as if talking to a potential client or future employer who came across the website. \
19
+ The Agent has been provided with context on {name} in the form of their summary and Linkedin details. Here's the information:"
uv.lock ADDED
The diff for this file is too large to render. See raw diff