gkijkul commited on
Commit
790867e
·
1 Parent(s): 6f25479

refactor and setup langgraph flow

Browse files
src/chatbot/form_management/acceptance_criteria.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "fields": [
3
+ {
4
+ "fieldName": "Project Name",
5
+ "priority": 1,
6
+ "description": "The name of the project or purpose for the payment.",
7
+ "validationRules": [
8
+ "Must be associated with environmental or sustainability projects."
9
+ ],
10
+ "exampleInput": "โครงการ \"การจัดการคาร์บอนเครดิตในป่าเพื่อการพัฒนาที่ยั่งยืน\""
11
+ },
12
+ {
13
+ "fieldName": "Date",
14
+ "priority": 2,
15
+ "description": "The date of the transaction.",
16
+ "validationRules": [
17
+ "Must be a date that is in the past or present."
18
+ ],
19
+ "exampleInput": "3 เมษายน 2567"
20
+ },
21
+ {
22
+ "fieldName": "Total Payment Amount",
23
+ "priority": 3,
24
+ "description": "The total amount paid.",
25
+ "validationRules": [
26
+ "Must be a positive amount."
27
+ ],
28
+ "exampleInput": "3 เมษายน 2567"
29
+ }
30
+ ]
31
+ }
32
+
src/chatbot/form_management/form_management.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class Form:
2
+ class PaymentDetailsTable:
3
+ def __init__(self, column_headers = None, row_entries = None):
4
+ self.column_headers = column_headers
5
+ self.row_entries = row_entries
6
+
7
+ def __init__(self, receipt_title = None, project_name = None, date = None, receipt_number = None, payer_name = None, column_headers = None, row_entries = None, total_payment_amount = None, incompleteness_description = None):
8
+ self.receipt_title = receipt_title
9
+ self.project_name = project_name
10
+ self.date = date
11
+ self.receipt_number = receipt_number
12
+ self.payer_name = payer_name
13
+ self.payment_details_table = self.PaymentDetailsTable(column_headers, row_entries)
14
+ self.total_payment_amount = total_payment_amount
15
+ self.incompleteness_description = incompleteness_description
16
+
17
+ def get_info_as_dict(self):
18
+ return {
19
+ "receipt_title": self.receipt_title,
20
+ "project_name": self.project_name,
21
+ "date": self.date,
22
+ "receipt_number": self.receipt_number,
23
+ "payer_name": self.payer_name,
24
+ "payment_details_table": {
25
+ "column_headers": self.payment_details_table.column_headers,
26
+ "row_entries": self.payment_details_table.row_entries
27
+ },
28
+ "total_payment_amount": self.total_payment_amount,
29
+ "incompleteness_description": self.incompleteness_description
30
+ }
31
+
32
+ # Getter and Setter for receipt_title
33
+ def get_receipt_title(self):
34
+ return self.receipt_title
35
+
36
+ def set_receipt_title(self, receipt_title):
37
+ self.receipt_title = receipt_title
38
+
39
+ # Getter and Setter for project_name
40
+ def get_project_name(self):
41
+ return self.project_name
42
+
43
+ def set_project_name(self, project_name):
44
+ self.project_name = project_name
45
+
46
+ # Getter and Setter for date
47
+ def get_date(self):
48
+ return self.date
49
+
50
+ def set_date(self, date):
51
+ self.date = date
52
+
53
+ # Getter and Setter for receipt_number
54
+ def get_receipt_number(self):
55
+ return self.receipt_number
56
+
57
+ def set_receipt_number(self, receipt_number):
58
+ self.receipt_number = receipt_number
59
+
60
+ # Getter and Setter for payer_name
61
+ def get_payer_name(self):
62
+ return self.payer_name
63
+
64
+ def set_payer_name(self, payer_name):
65
+ self.payer_name = payer_name
66
+
67
+ # Getter and Setter for payment_details_table
68
+ def get_payment_details_table(self):
69
+ return self.payment_details_table
70
+
71
+ def set_payment_details_table(self, payment_details_table):
72
+ self.payment_details_table = self.PaymentDetailsTable(**payment_details_table)
73
+
74
+ # Getter and Setter for total_payment_amount
75
+ def get_total_payment_amount(self):
76
+ return self.total_payment_amount
77
+
78
+ def set_total_payment_amount(self, total_payment_amount):
79
+ self.total_payment_amount = total_payment_amount
80
+
81
+ # Getter and Setter for incompleteness_description
82
+ def get_incompleteness_description(self):
83
+ return self.incompleteness_description
84
+
85
+ def set_incompleteness_description(self, incompleteness_description):
86
+ self.incompleteness_description = incompleteness_description
87
+
88
+ # # Example Usage
89
+ # form = Form()
src/chatbot/graph.png ADDED
src/chatbot/graph_manager.py CHANGED
@@ -5,10 +5,13 @@ from langgraph.graph import StateGraph, START, END
5
  from langgraph.checkpoint.memory import MemorySaver
6
  from nodes import (
7
  State,
8
- speaker_chatbot
 
9
  )
 
10
  from form_management.form_management import Form
11
  import sys
 
12
 
13
  class GraphManager:
14
  def __init__(self):
@@ -22,13 +25,29 @@ class GraphManager:
22
  graph_memory = MemorySaver()
23
  graph_builder = StateGraph(State)
24
 
25
- graph_builder.add_node("speaker_chatbot", speaker_chatbot)
 
 
 
 
 
 
 
 
26
 
27
  graph_builder.add_edge(START, "speaker_chatbot")
28
  graph_builder.add_edge("speaker_chatbot", END)
29
 
30
  self.graph = graph_builder.compile(checkpointer=graph_memory)
31
 
 
 
 
 
 
 
 
 
32
  def process_user_input(self, user_input, session_id):
33
  """
34
  Process the user input and update the graph state.
@@ -51,9 +70,10 @@ class GraphManager:
51
  return snapshot[0]["messages"][-1].content
52
 
53
  graph_manager = GraphManager()
 
54
 
55
- # Example usage
56
- response = graph_manager.process_user_input(
57
- "hi",
58
- 1
59
- )
 
5
  from langgraph.checkpoint.memory import MemorySaver
6
  from nodes import (
7
  State,
8
+ speaker_chatbot_node,
9
+ tool_node
10
  )
11
+ from langgraph.prebuilt import tools_condition
12
  from form_management.form_management import Form
13
  import sys
14
+ from IPython.display import Image, display
15
 
16
  class GraphManager:
17
  def __init__(self):
 
25
  graph_memory = MemorySaver()
26
  graph_builder = StateGraph(State)
27
 
28
+ graph_builder.add_node("speaker_chatbot", speaker_chatbot_node)
29
+ graph_builder.add_node("tools", tool_node)
30
+
31
+ graph_builder.add_conditional_edges(
32
+ "speaker_chatbot",
33
+ tools_condition,
34
+ )
35
+ # Any time a tool is called, we return to the chatbot to decide the next step
36
+ graph_builder.add_edge("tools", "speaker_chatbot")
37
 
38
  graph_builder.add_edge(START, "speaker_chatbot")
39
  graph_builder.add_edge("speaker_chatbot", END)
40
 
41
  self.graph = graph_builder.compile(checkpointer=graph_memory)
42
 
43
+ def save_graph_image(self):
44
+ """
45
+ Save the graph as a PNG image.
46
+ """
47
+ image = Image(self.graph.get_graph().draw_mermaid_png())
48
+ with open("graph.png", "wb") as f:
49
+ f.write(image.data)
50
+
51
  def process_user_input(self, user_input, session_id):
52
  """
53
  Process the user input and update the graph state.
 
70
  return snapshot[0]["messages"][-1].content
71
 
72
  graph_manager = GraphManager()
73
+ graph_manager.save_graph_image()
74
 
75
+ # # Example usage
76
+ # response = graph_manager.process_user_input(
77
+ # "hi",
78
+ # 1
79
+ # )
src/chatbot/nodes.py CHANGED
@@ -4,28 +4,77 @@ from typing import Annotated
4
  from typing_extensions import TypedDict
5
  from langgraph.graph.message import add_messages
6
  from langchain.tools import tool
7
- from langgraph.prebuilt import create_react_agent
8
  from langgraph.checkpoint.memory import MemorySaver
9
 
10
  from agent_llm_engine import llm_overall_agent
11
  from prompts.agent_prompts import speaker_system_message, auditor_system_message
12
  from form_management.form_management import Form
 
 
13
 
14
- # placeholder form
15
- form = Form()
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
  class State(TypedDict):
18
  messages: Annotated[list, add_messages]
19
  session_id: str
 
20
 
 
 
 
 
 
 
 
 
21
  @tool
22
- def placeholder_tool(input: str) -> str:
23
  """
24
- Placeholder tool that does nothing.
 
 
25
  """
26
- return "This is a placeholder tool."
 
 
 
 
 
 
 
 
 
27
 
28
- tools = [placeholder_tool]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
  speaker_memory = MemorySaver()
31
  speaker_agent = create_react_agent(
@@ -35,7 +84,7 @@ speaker_agent = create_react_agent(
35
  checkpointer=speaker_memory
36
  )
37
 
38
- def speaker_chatbot(state: State):
39
  config = {"configurable": {"thread_id": state["session_id"]}}
40
 
41
  response = speaker_agent.invoke(
@@ -54,6 +103,8 @@ def speaker_chatbot(state: State):
54
  "session_id": state["session_id"]
55
  }
56
 
 
 
57
  auditor_memory = MemorySaver()
58
  auditor_agent = create_react_agent(
59
  llm_overall_agent,
@@ -62,11 +113,8 @@ auditor_agent = create_react_agent(
62
  checkpointer=speaker_memory
63
  )
64
 
65
- def auditor_feedback(state: State):
66
- # get form based on session id, but for now just use placeholder
67
- form_info = form.get_info_as_dict()
68
-
69
  # auditor looks at form info and acceptance criteria and gives speaker feedback
70
 
71
-
72
- pass
 
4
  from typing_extensions import TypedDict
5
  from langgraph.graph.message import add_messages
6
  from langchain.tools import tool
7
+ from langgraph.prebuilt import create_react_agent, ToolNode
8
  from langgraph.checkpoint.memory import MemorySaver
9
 
10
  from agent_llm_engine import llm_overall_agent
11
  from prompts.agent_prompts import speaker_system_message, auditor_system_message
12
  from form_management.form_management import Form
13
+ from vlm.kie import receipt_kie
14
+ import ast
15
 
16
+ # placeholder, init empty form info
17
+ form_info = {
18
+ "receipt_title": "",
19
+ "project_name": "",
20
+ "date": "",
21
+ "receipt_number": "",
22
+ "payer_name": "",
23
+ "payment_details_table": {
24
+ "column_headers": "",
25
+ "row_entries": "",
26
+ },
27
+ "total_payment_amount": "",
28
+ "incompleteness_description": ""
29
+ }
30
 
31
  class State(TypedDict):
32
  messages: Annotated[list, add_messages]
33
  session_id: str
34
+ form_info: dict
35
 
36
+ # @tool
37
+ # def placeholder_tool(input: str) -> str:
38
+ # """
39
+ # Placeholder tool that does nothing.
40
+ # """
41
+ # return "This is a placeholder tool."
42
+
43
+ # session_id: str might need to be added later when there are concurrent users
44
  @tool
45
+ def apply_ocr(input: str) -> str:
46
  """
47
+ Apply optical-character-recognition (OCR) to the base64 string (input image) and initalize the form.
48
+ Feedback will then be provided on whether the form is complete or not.
49
+ Use this when the user provides a base64 string.
50
  """
51
+ kie_info = receipt_kie(input)
52
+ # overwrite form with kie info
53
+ try:
54
+ form_info = ast.literal_eval(kie_info)
55
+ except:
56
+ print("KIE ERROR")
57
+
58
+ feedback = auditor_feedback(form_info)
59
+
60
+ return feedback
61
 
62
+ @tool
63
+ def edit_form(key: str, value: str) -> str:
64
+ """
65
+ This is used to edit the form data.
66
+ The target field will be assigned with the provided value.
67
+ Feedback will then be provided on whether the form is complete or not.
68
+ Use this when the user wants to edit the form data.
69
+ """
70
+
71
+ form_info[key] = value
72
+ feedback = auditor_feedback(form_info)
73
+
74
+ return feedback
75
+
76
+
77
+ tools = [apply_ocr, edit_form]
78
 
79
  speaker_memory = MemorySaver()
80
  speaker_agent = create_react_agent(
 
84
  checkpointer=speaker_memory
85
  )
86
 
87
+ def speaker_chatbot_node(state: State):
88
  config = {"configurable": {"thread_id": state["session_id"]}}
89
 
90
  response = speaker_agent.invoke(
 
103
  "session_id": state["session_id"]
104
  }
105
 
106
+ tool_node = ToolNode(tools=[apply_ocr, edit_form])
107
+
108
  auditor_memory = MemorySaver()
109
  auditor_agent = create_react_agent(
110
  llm_overall_agent,
 
113
  checkpointer=speaker_memory
114
  )
115
 
116
+ # provides feedback based on form info
117
+ def auditor_feedback(form_info):
 
 
118
  # auditor looks at form info and acceptance criteria and gives speaker feedback
119
 
120
+ return "Form is complete."
 
src/{vlm → chatbot/vlm}/kie.py RENAMED
@@ -6,8 +6,8 @@ import openai
6
  import base64
7
  from dotenv import load_dotenv
8
  import os
9
- from prompts import kie_prompt
10
-
11
  # Load environment variables from .env file
12
  load_dotenv()
13
  client = openai.OpenAI()
@@ -47,6 +47,7 @@ def receipt_kie(image_path: str) -> str:
47
 
48
  return response.output_text
49
 
50
- # test case
51
- res = receipt_kie(r"C:\Users\Gavin\Desktop\hello-earth\data\jpeg_raw_data\bill\กองทุนพัฒนาบ้านห้วยทราย อ.แม่ออน จ.เชียงใหม่_page-0004.jpg")
52
- print(res)
 
 
6
  import base64
7
  from dotenv import load_dotenv
8
  import os
9
+ from vlm.prompts import kie_prompt
10
+ import ast
11
  # Load environment variables from .env file
12
  load_dotenv()
13
  client = openai.OpenAI()
 
47
 
48
  return response.output_text
49
 
50
+ # # test case
51
+ # res = receipt_kie(r"C:\Users\Gavin\Desktop\hello-earth\data\jpeg_raw_data\bill\กองทุนพัฒนาบ้านห้วยทราย อ.แม่ออน จ.เชียงใหม่_page-0004.jpg")
52
+ # res = ast.literal_eval(res)
53
+ # print(res)
src/{vlm → chatbot/vlm}/prompts.py RENAMED
File without changes
src/poetry.lock CHANGED
@@ -46,6 +46,22 @@ doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)",
46
  test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""]
47
  trio = ["trio (>=0.26.1)"]
48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  [[package]]
50
  name = "audioop-lts"
51
  version = "0.2.1"
@@ -308,12 +324,24 @@ description = "Cross-platform colored terminal text."
308
  optional = false
309
  python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
310
  groups = ["main"]
311
- markers = "platform_system == \"Windows\""
312
  files = [
313
  {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
314
  {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
315
  ]
316
 
 
 
 
 
 
 
 
 
 
 
 
 
317
  [[package]]
318
  name = "distro"
319
  version = "1.9.0"
@@ -340,6 +368,21 @@ files = [
340
  [package.dependencies]
341
  python-dotenv = "*"
342
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
343
  [[package]]
344
  name = "fastapi"
345
  version = "0.115.12"
@@ -692,6 +735,73 @@ files = [
692
  [package.extras]
693
  all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
694
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
695
  [[package]]
696
  name = "jinja2"
697
  version = "3.1.6"
@@ -1108,6 +1218,21 @@ files = [
1108
  {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"},
1109
  ]
1110
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1111
  [[package]]
1112
  name = "mdurl"
1113
  version = "0.1.2"
@@ -1441,6 +1566,38 @@ sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-d
1441
  test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"]
1442
  xml = ["lxml (>=4.9.2)"]
1443
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1444
  [[package]]
1445
  name = "pillow"
1446
  version = "11.2.1"
@@ -1541,6 +1698,49 @@ tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "ole
1541
  typing = ["typing-extensions ; python_version < \"3.10\""]
1542
  xmp = ["defusedxml"]
1543
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1544
  [[package]]
1545
  name = "pycparser"
1546
  version = "2.22"
@@ -1707,7 +1907,6 @@ description = "Pygments is a syntax highlighting package written in Python."
1707
  optional = false
1708
  python-versions = ">=3.8"
1709
  groups = ["main"]
1710
- markers = "sys_platform != \"emscripten\""
1711
  files = [
1712
  {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"},
1713
  {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"},
@@ -2190,6 +2389,26 @@ postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"]
2190
  pymysql = ["pymysql"]
2191
  sqlcipher = ["sqlcipher3_binary"]
2192
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2193
  [[package]]
2194
  name = "starlette"
2195
  version = "0.46.2"
@@ -2306,6 +2525,22 @@ notebook = ["ipywidgets (>=6)"]
2306
  slack = ["slack-sdk"]
2307
  telegram = ["requests"]
2308
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2309
  [[package]]
2310
  name = "typer"
2311
  version = "0.15.3"
@@ -2402,6 +2637,18 @@ h11 = ">=0.8"
2402
  [package.extras]
2403
  standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"]
2404
 
 
 
 
 
 
 
 
 
 
 
 
 
2405
  [[package]]
2406
  name = "websockets"
2407
  version = "15.0.1"
@@ -2730,4 +2977,4 @@ cffi = ["cffi (>=1.11)"]
2730
  [metadata]
2731
  lock-version = "2.1"
2732
  python-versions = ">=3.12.4,<4.0.0"
2733
- content-hash = "3293db5a94662f3c1087e04a9f805f71f32ec3efbab1302d4d9eaa48d1b6a4b0"
 
46
  test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""]
47
  trio = ["trio (>=0.26.1)"]
48
 
49
+ [[package]]
50
+ name = "asttokens"
51
+ version = "3.0.0"
52
+ description = "Annotate AST trees with source code positions"
53
+ optional = false
54
+ python-versions = ">=3.8"
55
+ groups = ["main"]
56
+ files = [
57
+ {file = "asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2"},
58
+ {file = "asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7"},
59
+ ]
60
+
61
+ [package.extras]
62
+ astroid = ["astroid (>=2,<4)"]
63
+ test = ["astroid (>=2,<4)", "pytest", "pytest-cov", "pytest-xdist"]
64
+
65
  [[package]]
66
  name = "audioop-lts"
67
  version = "0.2.1"
 
324
  optional = false
325
  python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
326
  groups = ["main"]
327
+ markers = "platform_system == \"Windows\" or sys_platform == \"win32\""
328
  files = [
329
  {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
330
  {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
331
  ]
332
 
333
+ [[package]]
334
+ name = "decorator"
335
+ version = "5.2.1"
336
+ description = "Decorators for Humans"
337
+ optional = false
338
+ python-versions = ">=3.8"
339
+ groups = ["main"]
340
+ files = [
341
+ {file = "decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a"},
342
+ {file = "decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360"},
343
+ ]
344
+
345
  [[package]]
346
  name = "distro"
347
  version = "1.9.0"
 
368
  [package.dependencies]
369
  python-dotenv = "*"
370
 
371
+ [[package]]
372
+ name = "executing"
373
+ version = "2.2.0"
374
+ description = "Get the currently executing AST node of a frame, and other information"
375
+ optional = false
376
+ python-versions = ">=3.8"
377
+ groups = ["main"]
378
+ files = [
379
+ {file = "executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa"},
380
+ {file = "executing-2.2.0.tar.gz", hash = "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755"},
381
+ ]
382
+
383
+ [package.extras]
384
+ tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich ; python_version >= \"3.11\""]
385
+
386
  [[package]]
387
  name = "fastapi"
388
  version = "0.115.12"
 
735
  [package.extras]
736
  all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
737
 
738
+ [[package]]
739
+ name = "ipython"
740
+ version = "9.2.0"
741
+ description = "IPython: Productive Interactive Computing"
742
+ optional = false
743
+ python-versions = ">=3.11"
744
+ groups = ["main"]
745
+ files = [
746
+ {file = "ipython-9.2.0-py3-none-any.whl", hash = "sha256:fef5e33c4a1ae0759e0bba5917c9db4eb8c53fee917b6a526bd973e1ca5159f6"},
747
+ {file = "ipython-9.2.0.tar.gz", hash = "sha256:62a9373dbc12f28f9feaf4700d052195bf89806279fc8ca11f3f54017d04751b"},
748
+ ]
749
+
750
+ [package.dependencies]
751
+ colorama = {version = "*", markers = "sys_platform == \"win32\""}
752
+ decorator = "*"
753
+ ipython-pygments-lexers = "*"
754
+ jedi = ">=0.16"
755
+ matplotlib-inline = "*"
756
+ pexpect = {version = ">4.3", markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""}
757
+ prompt_toolkit = ">=3.0.41,<3.1.0"
758
+ pygments = ">=2.4.0"
759
+ stack_data = "*"
760
+ traitlets = ">=5.13.0"
761
+
762
+ [package.extras]
763
+ all = ["ipython[doc,matplotlib,test,test-extra]"]
764
+ black = ["black"]
765
+ doc = ["docrepr", "exceptiongroup", "intersphinx_registry", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinx_toml (==0.0.4)", "typing_extensions"]
766
+ matplotlib = ["matplotlib"]
767
+ test = ["packaging", "pytest", "pytest-asyncio (<0.22)", "testpath"]
768
+ test-extra = ["curio", "ipykernel", "ipython[test]", "jupyter_ai", "matplotlib (!=3.2.0)", "nbclient", "nbformat", "numpy (>=1.23)", "pandas", "trio"]
769
+
770
+ [[package]]
771
+ name = "ipython-pygments-lexers"
772
+ version = "1.1.1"
773
+ description = "Defines a variety of Pygments lexers for highlighting IPython code."
774
+ optional = false
775
+ python-versions = ">=3.8"
776
+ groups = ["main"]
777
+ files = [
778
+ {file = "ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c"},
779
+ {file = "ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81"},
780
+ ]
781
+
782
+ [package.dependencies]
783
+ pygments = "*"
784
+
785
+ [[package]]
786
+ name = "jedi"
787
+ version = "0.19.2"
788
+ description = "An autocompletion tool for Python that can be used for text editors."
789
+ optional = false
790
+ python-versions = ">=3.6"
791
+ groups = ["main"]
792
+ files = [
793
+ {file = "jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9"},
794
+ {file = "jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0"},
795
+ ]
796
+
797
+ [package.dependencies]
798
+ parso = ">=0.8.4,<0.9.0"
799
+
800
+ [package.extras]
801
+ docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"]
802
+ qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"]
803
+ testing = ["Django", "attrs", "colorama", "docopt", "pytest (<9.0.0)"]
804
+
805
  [[package]]
806
  name = "jinja2"
807
  version = "3.1.6"
 
1218
  {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"},
1219
  ]
1220
 
1221
+ [[package]]
1222
+ name = "matplotlib-inline"
1223
+ version = "0.1.7"
1224
+ description = "Inline Matplotlib backend for Jupyter"
1225
+ optional = false
1226
+ python-versions = ">=3.8"
1227
+ groups = ["main"]
1228
+ files = [
1229
+ {file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"},
1230
+ {file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"},
1231
+ ]
1232
+
1233
+ [package.dependencies]
1234
+ traitlets = "*"
1235
+
1236
  [[package]]
1237
  name = "mdurl"
1238
  version = "0.1.2"
 
1566
  test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"]
1567
  xml = ["lxml (>=4.9.2)"]
1568
 
1569
+ [[package]]
1570
+ name = "parso"
1571
+ version = "0.8.4"
1572
+ description = "A Python Parser"
1573
+ optional = false
1574
+ python-versions = ">=3.6"
1575
+ groups = ["main"]
1576
+ files = [
1577
+ {file = "parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18"},
1578
+ {file = "parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d"},
1579
+ ]
1580
+
1581
+ [package.extras]
1582
+ qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"]
1583
+ testing = ["docopt", "pytest"]
1584
+
1585
+ [[package]]
1586
+ name = "pexpect"
1587
+ version = "4.9.0"
1588
+ description = "Pexpect allows easy control of interactive console applications."
1589
+ optional = false
1590
+ python-versions = "*"
1591
+ groups = ["main"]
1592
+ markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""
1593
+ files = [
1594
+ {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"},
1595
+ {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"},
1596
+ ]
1597
+
1598
+ [package.dependencies]
1599
+ ptyprocess = ">=0.5"
1600
+
1601
  [[package]]
1602
  name = "pillow"
1603
  version = "11.2.1"
 
1698
  typing = ["typing-extensions ; python_version < \"3.10\""]
1699
  xmp = ["defusedxml"]
1700
 
1701
+ [[package]]
1702
+ name = "prompt-toolkit"
1703
+ version = "3.0.51"
1704
+ description = "Library for building powerful interactive command lines in Python"
1705
+ optional = false
1706
+ python-versions = ">=3.8"
1707
+ groups = ["main"]
1708
+ files = [
1709
+ {file = "prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07"},
1710
+ {file = "prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed"},
1711
+ ]
1712
+
1713
+ [package.dependencies]
1714
+ wcwidth = "*"
1715
+
1716
+ [[package]]
1717
+ name = "ptyprocess"
1718
+ version = "0.7.0"
1719
+ description = "Run a subprocess in a pseudo terminal"
1720
+ optional = false
1721
+ python-versions = "*"
1722
+ groups = ["main"]
1723
+ markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""
1724
+ files = [
1725
+ {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"},
1726
+ {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"},
1727
+ ]
1728
+
1729
+ [[package]]
1730
+ name = "pure-eval"
1731
+ version = "0.2.3"
1732
+ description = "Safely evaluate AST nodes without side effects"
1733
+ optional = false
1734
+ python-versions = "*"
1735
+ groups = ["main"]
1736
+ files = [
1737
+ {file = "pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0"},
1738
+ {file = "pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"},
1739
+ ]
1740
+
1741
+ [package.extras]
1742
+ tests = ["pytest"]
1743
+
1744
  [[package]]
1745
  name = "pycparser"
1746
  version = "2.22"
 
1907
  optional = false
1908
  python-versions = ">=3.8"
1909
  groups = ["main"]
 
1910
  files = [
1911
  {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"},
1912
  {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"},
 
2389
  pymysql = ["pymysql"]
2390
  sqlcipher = ["sqlcipher3_binary"]
2391
 
2392
+ [[package]]
2393
+ name = "stack-data"
2394
+ version = "0.6.3"
2395
+ description = "Extract data from python stack frames and tracebacks for informative displays"
2396
+ optional = false
2397
+ python-versions = "*"
2398
+ groups = ["main"]
2399
+ files = [
2400
+ {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"},
2401
+ {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"},
2402
+ ]
2403
+
2404
+ [package.dependencies]
2405
+ asttokens = ">=2.1.0"
2406
+ executing = ">=1.2.0"
2407
+ pure-eval = "*"
2408
+
2409
+ [package.extras]
2410
+ tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"]
2411
+
2412
  [[package]]
2413
  name = "starlette"
2414
  version = "0.46.2"
 
2525
  slack = ["slack-sdk"]
2526
  telegram = ["requests"]
2527
 
2528
+ [[package]]
2529
+ name = "traitlets"
2530
+ version = "5.14.3"
2531
+ description = "Traitlets Python configuration system"
2532
+ optional = false
2533
+ python-versions = ">=3.8"
2534
+ groups = ["main"]
2535
+ files = [
2536
+ {file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"},
2537
+ {file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"},
2538
+ ]
2539
+
2540
+ [package.extras]
2541
+ docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"]
2542
+ test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"]
2543
+
2544
  [[package]]
2545
  name = "typer"
2546
  version = "0.15.3"
 
2637
  [package.extras]
2638
  standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"]
2639
 
2640
+ [[package]]
2641
+ name = "wcwidth"
2642
+ version = "0.2.13"
2643
+ description = "Measures the displayed width of unicode strings in a terminal"
2644
+ optional = false
2645
+ python-versions = "*"
2646
+ groups = ["main"]
2647
+ files = [
2648
+ {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"},
2649
+ {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"},
2650
+ ]
2651
+
2652
  [[package]]
2653
  name = "websockets"
2654
  version = "15.0.1"
 
2977
  [metadata]
2978
  lock-version = "2.1"
2979
  python-versions = ">=3.12.4,<4.0.0"
2980
+ content-hash = "efbddde7effbf9adb8d0fd2294ffca2d5f4460bf12f75cfab57a0a47ca868c0e"
src/pyproject.toml CHANGED
@@ -13,7 +13,8 @@ dependencies = [
13
  "dotenv (>=0.9.9,<0.10.0)",
14
  "langgraph (>=0.4.1,<0.5.0)",
15
  "langchain (>=0.3.24,<0.4.0)",
16
- "langchain-openai (>=0.3.14,<0.4.0)"
 
17
  ]
18
 
19
 
 
13
  "dotenv (>=0.9.9,<0.10.0)",
14
  "langgraph (>=0.4.1,<0.5.0)",
15
  "langchain (>=0.3.24,<0.4.0)",
16
+ "langchain-openai (>=0.3.14,<0.4.0)",
17
+ "ipython (>=9.2.0,<10.0.0)"
18
  ]
19
 
20
 
src/utils/utils.py DELETED
File without changes