Spaces:
Sleeping
Sleeping
Antonis Bast commited on
Commit ·
4d557a4
1
Parent(s): f62f692
Fix app.py merge conflict markers
Browse files
app.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
| 1 |
-
<<<<<<< HEAD
|
| 2 |
from dotenv import load_dotenv
|
| 3 |
from openai import OpenAI
|
| 4 |
import json
|
|
@@ -385,392 +384,4 @@ if __name__ == "__main__":
|
|
| 385 |
demo.load(load_welcome, None, chatbot)
|
| 386 |
|
| 387 |
demo.launch(share=False)
|
| 388 |
-
=======
|
| 389 |
-
from dotenv import load_dotenv
|
| 390 |
-
from openai import OpenAI
|
| 391 |
-
import json
|
| 392 |
-
import os
|
| 393 |
-
import requests
|
| 394 |
-
from pypdf import PdfReader
|
| 395 |
-
import gradio as gr
|
| 396 |
-
from datetime import datetime
|
| 397 |
-
import gspread
|
| 398 |
-
from oauth2client.service_account import ServiceAccountCredentials
|
| 399 |
-
import base64
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
load_dotenv(override=True)
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
def get_sheets_client():
|
| 406 |
-
"""Initialize Google Sheets client"""
|
| 407 |
-
scope = [
|
| 408 |
-
'https://spreadsheets.google.com/feeds',
|
| 409 |
-
'https://www.googleapis.com/auth/drive'
|
| 410 |
-
]
|
| 411 |
-
|
| 412 |
-
credentials_file = 'credentials.json'
|
| 413 |
-
|
| 414 |
-
if os.path.exists(credentials_file):
|
| 415 |
-
creds = ServiceAccountCredentials.from_json_keyfile_name(
|
| 416 |
-
credentials_file,
|
| 417 |
-
scope
|
| 418 |
-
)
|
| 419 |
-
else:
|
| 420 |
-
creds_json = os.environ.get("GOOGLE_SHEETS_CREDS")
|
| 421 |
-
|
| 422 |
-
if not creds_json:
|
| 423 |
-
raise ValueError("GOOGLE_SHEETS_CREDS not found in environment variables!")
|
| 424 |
-
|
| 425 |
-
try:
|
| 426 |
-
creds_dict = json.loads(creds_json)
|
| 427 |
-
except json.JSONDecodeError as e:
|
| 428 |
-
raise ValueError(f"Invalid JSON in GOOGLE_SHEETS_CREDS: {e}")
|
| 429 |
-
|
| 430 |
-
creds = ServiceAccountCredentials.from_json_keyfile_dict(
|
| 431 |
-
creds_dict,
|
| 432 |
-
scope
|
| 433 |
-
)
|
| 434 |
-
|
| 435 |
-
return gspread.authorize(creds)
|
| 436 |
-
|
| 437 |
-
def record_user_details(email, name="Name not provided", notes="Not provided"):
|
| 438 |
-
"""Record user contact details to Google Sheets"""
|
| 439 |
-
try:
|
| 440 |
-
client = get_sheets_client()
|
| 441 |
-
sheet_name = os.getenv("GOOGLE_SHEET_NAME", "ChatBot Contacts")
|
| 442 |
-
spreadsheet = client.open(sheet_name)
|
| 443 |
-
worksheet = spreadsheet.worksheet("Contacts")
|
| 444 |
-
|
| 445 |
-
row = [
|
| 446 |
-
datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
| 447 |
-
name,
|
| 448 |
-
email,
|
| 449 |
-
notes
|
| 450 |
-
]
|
| 451 |
-
|
| 452 |
-
worksheet.append_row(row, value_input_option='RAW')
|
| 453 |
-
print(f"✓ Contact recorded: {name} ({email})")
|
| 454 |
-
return {"status": "success", "message": f"Thank you! I've recorded your details. I'll get back to you at {email} soon."}
|
| 455 |
-
|
| 456 |
-
except Exception as e:
|
| 457 |
-
print(f"❌ Error recording contact: {e}")
|
| 458 |
-
return {"status": "error", "message": "There was an error recording your details. Please try again."}
|
| 459 |
-
|
| 460 |
-
def record_unanswered_question(question, context=""):
|
| 461 |
-
"""Record questions the bot couldn't answer"""
|
| 462 |
-
try:
|
| 463 |
-
client = get_sheets_client()
|
| 464 |
-
sheet_name = os.getenv("GOOGLE_SHEET_NAME", "ChatBot Contacts")
|
| 465 |
-
spreadsheet = client.open(sheet_name)
|
| 466 |
-
worksheet = spreadsheet.worksheet("Unanswered Questions")
|
| 467 |
-
|
| 468 |
-
row = [
|
| 469 |
-
datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
| 470 |
-
question,
|
| 471 |
-
context
|
| 472 |
-
]
|
| 473 |
-
|
| 474 |
-
worksheet.append_row(row, value_input_option='RAW')
|
| 475 |
-
print(f"✓ Unanswered question recorded")
|
| 476 |
-
return {"status": "success", "message": "I've recorded your question for follow-up."}
|
| 477 |
-
|
| 478 |
-
except Exception as e:
|
| 479 |
-
print(f"❌ Error recording question: {e}")
|
| 480 |
-
return {"status": "error", "message": "Error recording question."}
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
record_user_details_json = {
|
| 485 |
-
"name": "record_user_details",
|
| 486 |
-
"description": "Use this tool to record that a user is interested in being in touch and provided an email address",
|
| 487 |
-
"parameters": {
|
| 488 |
-
"type": "object",
|
| 489 |
-
"properties": {
|
| 490 |
-
"email": {
|
| 491 |
-
"type": "string",
|
| 492 |
-
"description": "The email address of this user"
|
| 493 |
-
},
|
| 494 |
-
"name": {
|
| 495 |
-
"type": "string",
|
| 496 |
-
"description": "The user's name, if they provided it"
|
| 497 |
-
}
|
| 498 |
-
,
|
| 499 |
-
"notes": {
|
| 500 |
-
"type": "string",
|
| 501 |
-
"description": "Any additional information about the conversation that's worth recording to give context"
|
| 502 |
-
}
|
| 503 |
-
},
|
| 504 |
-
"required": ["email"],
|
| 505 |
-
"additionalProperties": False
|
| 506 |
-
}
|
| 507 |
-
}
|
| 508 |
-
|
| 509 |
-
record_unanswered_question_json = {
|
| 510 |
-
"name": "record_unanswered_question",
|
| 511 |
-
"description": "Always use this tool to record any question that couldn't be answered as you didn't know the answer",
|
| 512 |
-
"parameters": {
|
| 513 |
-
"type": "object",
|
| 514 |
-
"properties": {
|
| 515 |
-
"question": {
|
| 516 |
-
"type": "string",
|
| 517 |
-
"description": "The question that couldn't be answered"
|
| 518 |
-
},
|
| 519 |
-
},
|
| 520 |
-
"required": ["question"],
|
| 521 |
-
"additionalProperties": False
|
| 522 |
-
}
|
| 523 |
-
}
|
| 524 |
-
|
| 525 |
-
tools = [{"type": "function", "function": record_user_details_json},
|
| 526 |
-
{"type": "function", "function": record_unanswered_question_json}]
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
class Me:
|
| 530 |
-
|
| 531 |
-
def __init__(self):
|
| 532 |
-
self.openai = OpenAI()
|
| 533 |
-
self.name = "Antonis Bastoulis"
|
| 534 |
-
reader = PdfReader("me/linkedin.pdf")
|
| 535 |
-
self.linkedin = ""
|
| 536 |
-
for page in reader.pages:
|
| 537 |
-
text = page.extract_text()
|
| 538 |
-
if text:
|
| 539 |
-
self.linkedin += text
|
| 540 |
-
readerCV = PdfReader("me/CV.pdf")
|
| 541 |
-
self.CV = ""
|
| 542 |
-
for page in readerCV.pages:
|
| 543 |
-
text = page.extract_text()
|
| 544 |
-
if text:
|
| 545 |
-
self.CV += text
|
| 546 |
-
with open("me/summary.txt", "r", encoding="utf-8") as f:
|
| 547 |
-
self.summary = f.read()
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
def handle_tool_call(self, tool_calls):
|
| 551 |
-
results = []
|
| 552 |
-
for tool_call in tool_calls:
|
| 553 |
-
tool_name = tool_call.function.name
|
| 554 |
-
arguments = json.loads(tool_call.function.arguments)
|
| 555 |
-
print(f"Tool called: {tool_name}", flush=True)
|
| 556 |
-
tool = globals().get(tool_name)
|
| 557 |
-
result = tool(**arguments) if tool else {}
|
| 558 |
-
results.append({"role": "tool","content": json.dumps(result),"tool_call_id": tool_call.id})
|
| 559 |
-
return results
|
| 560 |
-
|
| 561 |
-
def system_prompt(self):
|
| 562 |
-
system_prompt = f"""You are {self.name}'s professional representative, answering questions about career, skills, and experience.
|
| 563 |
-
You are an AI assistant representing {self.name}. Speak in first person as if you are {self.name}, using "I" and "my" when discussing his experience, skills, and background. However, if directly asked if you are AI or Antonis himself, be transparent that you're an AI assistant trained on his professional information.
|
| 564 |
-
|
| 565 |
-
## BOUNDARIES - Never Discuss:
|
| 566 |
-
- Personal life (family, relationships, health, politics, religion)
|
| 567 |
-
- Compensation details (past salaries, current income)
|
| 568 |
-
- Why you left previous jobs
|
| 569 |
-
- Negative comments about past employers/colleagues
|
| 570 |
-
- Confidential project details or proprietary information
|
| 571 |
-
- Personal contact info (home address, personal phone)
|
| 572 |
-
|
| 573 |
-
## How to Handle Restricted Topics:
|
| 574 |
-
Redirect professionally: "I keep that private. Let me tell you about {self.name}'s expertise in [relevant area] instead."
|
| 575 |
-
For salary questions: "Compensation is best discussed based on role requirements. What position interests you?"
|
| 576 |
-
|
| 577 |
-
## Available Information:
|
| 578 |
-
### Summary:
|
| 579 |
-
{self.summary}
|
| 580 |
-
### LinkedIn Profile:
|
| 581 |
-
{self.linkedin}
|
| 582 |
-
### CV:
|
| 583 |
-
{self.CV}
|
| 584 |
-
Stay in character as {self.name}'s professional representative. Be helpful within appropriate boundaries."""
|
| 585 |
-
return system_prompt
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
def chat(self, message, history):
|
| 590 |
-
messages = [{"role": "system", "content": self.system_prompt()}] + history + [{"role": "user", "content": message}]
|
| 591 |
-
done = False
|
| 592 |
-
while not done:
|
| 593 |
-
response = self.openai.chat.completions.create(model="gpt-4o-mini", messages=messages, tools=tools)
|
| 594 |
-
if response.choices[0].finish_reason=="tool_calls":
|
| 595 |
-
message = response.choices[0].message
|
| 596 |
-
tool_calls = message.tool_calls
|
| 597 |
-
results = self.handle_tool_call(tool_calls)
|
| 598 |
-
messages.append(message)
|
| 599 |
-
messages.extend(results)
|
| 600 |
-
else:
|
| 601 |
-
done = True
|
| 602 |
-
return response.choices[0].message.content
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
if __name__ == "__main__":
|
| 606 |
-
me = Me()
|
| 607 |
-
|
| 608 |
-
# Custom theme
|
| 609 |
-
custom_theme = gr.themes.Soft(
|
| 610 |
-
primary_hue="blue",
|
| 611 |
-
secondary_hue="slate",
|
| 612 |
-
neutral_hue="slate",
|
| 613 |
-
font=[gr.themes.GoogleFont("Inter"), "sans-serif"]
|
| 614 |
-
).set(
|
| 615 |
-
body_background_fill="*neutral_50",
|
| 616 |
-
button_primary_background_fill="*primary_600",
|
| 617 |
-
button_primary_background_fill_hover="*primary_700",
|
| 618 |
-
)
|
| 619 |
-
|
| 620 |
-
with gr.Blocks(theme=custom_theme, title="Career Chatbot ", css="""
|
| 621 |
-
.header-container {
|
| 622 |
-
text-align: center;
|
| 623 |
-
padding: 2rem;
|
| 624 |
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 625 |
-
color: white;
|
| 626 |
-
border-radius: 10px;
|
| 627 |
-
margin-bottom: 2rem;
|
| 628 |
-
}
|
| 629 |
-
.header-title {
|
| 630 |
-
font-size: 2.5rem;
|
| 631 |
-
font-weight: bold;
|
| 632 |
-
margin-bottom: 0.5rem;
|
| 633 |
-
}
|
| 634 |
-
.header-subtitle {
|
| 635 |
-
font-size: 1.2rem;
|
| 636 |
-
opacity: 0.9;
|
| 637 |
-
}
|
| 638 |
-
footer {
|
| 639 |
-
text-align: center;
|
| 640 |
-
padding: 2rem;
|
| 641 |
-
color: #666;
|
| 642 |
-
}
|
| 643 |
-
""") as demo:
|
| 644 |
-
|
| 645 |
-
# Header
|
| 646 |
-
gr.HTML("""
|
| 647 |
-
<div class="header-container">
|
| 648 |
-
<div class="header-title">Antonis Bastoulis</div>
|
| 649 |
-
<div class="header-subtitle">
|
| 650 |
-
Naval Architect & Marine Engineer | M.Sc. in Artificial Intelligence & Deep Learning
|
| 651 |
-
</div>
|
| 652 |
-
<div style="font-size: 1rem; margin-top: 1rem; opacity: 0.85;">
|
| 653 |
-
AI Assistant • Ask me about my experience, projects, and expertise
|
| 654 |
-
</div>
|
| 655 |
-
</div>
|
| 656 |
-
""")
|
| 657 |
-
|
| 658 |
-
# Tabs for different sections
|
| 659 |
-
with gr.Tabs():
|
| 660 |
-
# Chat Tab
|
| 661 |
-
with gr.TabItem("💬 Chat with Me"):
|
| 662 |
-
with gr.Row():
|
| 663 |
-
with gr.Column(scale=2):
|
| 664 |
-
chatbot = gr.Chatbot(
|
| 665 |
-
label="Conversation",
|
| 666 |
-
height=500,
|
| 667 |
-
bubble_full_width=False,
|
| 668 |
-
show_copy_button=True,
|
| 669 |
-
type="messages"
|
| 670 |
-
)
|
| 671 |
-
|
| 672 |
-
with gr.Row():
|
| 673 |
-
msg = gr.Textbox(
|
| 674 |
-
placeholder="Ask me about my experience, skills, projects...",
|
| 675 |
-
show_label=False,
|
| 676 |
-
scale=4,
|
| 677 |
-
container=False
|
| 678 |
-
)
|
| 679 |
-
submit_btn = gr.Button("Send", variant="primary", scale=1)
|
| 680 |
-
|
| 681 |
-
with gr.Row():
|
| 682 |
-
clear = gr.Button("🔄 Clear Chat", size="sm")
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
# About/FAQ Tab
|
| 686 |
-
with gr.TabItem("ℹ️ About"):
|
| 687 |
-
with gr.Row():
|
| 688 |
-
with gr.Column():
|
| 689 |
-
gr.Markdown("""
|
| 690 |
-
### About This Chatbot
|
| 691 |
-
|
| 692 |
-
This AI assistant is trained on my professional profile and can answer questions about:
|
| 693 |
-
- 💼 Work experience and career history
|
| 694 |
-
- 🛠️ Technical skills and expertise
|
| 695 |
-
- 📁 Projects and achievements
|
| 696 |
-
- 🎓 Education and certifications
|
| 697 |
-
- 🎯 Career goals and interests
|
| 698 |
-
|
| 699 |
-
### How to Use
|
| 700 |
-
1. Ask any question about my professional background
|
| 701 |
-
2. If I can't answer something, it will be recorded for me to review
|
| 702 |
-
3. Want to connect? Simply tell the chatbot your name, email, and a message - I'll record your details and get back to you.
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
### Privacy & Data Protection
|
| 706 |
-
- Conversations are processed securely via OpenAI's API
|
| 707 |
-
- Contact information is only used to reach out to you
|
| 708 |
-
- Unanswered questions are recorded to improve responses
|
| 709 |
-
- No conversation history is permanently stored
|
| 710 |
-
|
| 711 |
-
---
|
| 712 |
-
""")
|
| 713 |
-
|
| 714 |
-
gr.Markdown("### Connect With Me")
|
| 715 |
-
gr.HTML("""
|
| 716 |
-
<div style="text-align: center; padding: 2rem;">
|
| 717 |
-
<a href="https://www.linkedin.com/in/antonisbast/" target="_blank"
|
| 718 |
-
style="margin: 0 1rem; text-decoration: none; font-size: 1.1rem;">
|
| 719 |
-
🔗 LinkedIn
|
| 720 |
-
</a>
|
| 721 |
-
<a href="https://github.com/antonisbast" target="_blank"
|
| 722 |
-
style="margin: 0 1rem; text-decoration: none; font-size: 1.1rem;">
|
| 723 |
-
💻 GitHub
|
| 724 |
-
</a>
|
| 725 |
-
<a href="mailto:antonisbast@gmail.com"
|
| 726 |
-
style="margin: 0 1rem; text-decoration: none; font-size: 1.1rem;">
|
| 727 |
-
📧 Email
|
| 728 |
-
</a>
|
| 729 |
-
<a href="https://huggingface.co/antonisbast" target="_blank"
|
| 730 |
-
style="margin: 0 1rem; text-decoration: none; font-size: 1.1rem;">
|
| 731 |
-
🌐 Hugging Face
|
| 732 |
-
</a>
|
| 733 |
-
</div>
|
| 734 |
-
""")
|
| 735 |
-
|
| 736 |
-
gr.Markdown("""
|
| 737 |
-
### Technical Details
|
| 738 |
-
- **Powered by:** OpenAI GPT-4o-mini
|
| 739 |
-
- **Framework:** Gradio
|
| 740 |
-
- **Data Sources:** LinkedIn Profile, CV/Resume
|
| 741 |
-
- **Features:** Function calling for contact recording and unanswered questions
|
| 742 |
-
""")
|
| 743 |
-
|
| 744 |
-
# Footer
|
| 745 |
-
gr.HTML("""
|
| 746 |
-
<footer>
|
| 747 |
-
<p>🤖 Powered by ChatGPT & Gradio | Last Updated: 2025</p>
|
| 748 |
-
</footer>
|
| 749 |
-
""")
|
| 750 |
-
|
| 751 |
-
def respond(message, history):
|
| 752 |
-
"""Handle chat response"""
|
| 753 |
-
if not message.strip():
|
| 754 |
-
return history, ""
|
| 755 |
-
|
| 756 |
-
history.append({"role": "user", "content": message})
|
| 757 |
-
yield history, "" # First yield - shows user message right away
|
| 758 |
-
bot_message = me.chat(message, history[:-1])
|
| 759 |
-
history.append({"role": "assistant", "content": bot_message})
|
| 760 |
-
yield history, "" # Second yield - shows bot response
|
| 761 |
-
|
| 762 |
-
def load_welcome():
|
| 763 |
-
return [{"role": "assistant", "content": "Hello! I'm Antonis's AI assistant. I can answer questions about his experience in naval architecture, marine engineering, and AI/ML projects. What would you like to know?"}]
|
| 764 |
-
|
| 765 |
-
# Chat events
|
| 766 |
-
msg.submit(respond, [msg, chatbot], [chatbot, msg])
|
| 767 |
-
submit_btn.click(respond, [msg, chatbot], [chatbot, msg])
|
| 768 |
-
clear.click(lambda: [], None, chatbot, queue=False)
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
# Load welcome message
|
| 772 |
-
demo.load(load_welcome, None, chatbot)
|
| 773 |
-
|
| 774 |
-
demo.launch(share=False)
|
| 775 |
-
>>>>>>> huggingface/main
|
| 776 |
|
|
|
|
|
|
|
| 1 |
from dotenv import load_dotenv
|
| 2 |
from openai import OpenAI
|
| 3 |
import json
|
|
|
|
| 384 |
demo.load(load_welcome, None, chatbot)
|
| 385 |
|
| 386 |
demo.launch(share=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 387 |
|