Arcadia822 commited on
Commit
e34be6c
·
unverified ·
1 Parent(s): f532f32

feat(task): :sparkles: analyze oj wrong answer (#10)

Browse files

Accept a coding problem and incorrect answer as input. Start a conversation to help student figure out whats wrong

edu_assistant/learning_tasks/__init__.py CHANGED
@@ -1,3 +1,4 @@
 
1
  from .qa import QaTask
2
 
3
- __all__ = ["QaTask"]
 
1
+ from .coding_problem import CodingProblemAnalysis
2
  from .qa import QaTask
3
 
4
+ __all__ = ["QaTask", "CodingProblemAnalysis"]
edu_assistant/learning_tasks/base.py CHANGED
@@ -1,2 +1,6 @@
 
 
 
1
  class BaseTask:
2
- pass
 
 
1
+ import uuid
2
+
3
+
4
  class BaseTask:
5
+ def _create_session_id(self) -> str:
6
+ return str(uuid.uuid1())
edu_assistant/learning_tasks/coding_problem.py ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain import ConversationChain, PromptTemplate
2
+ from langchain.chains import ConversationalRetrievalChain
3
+ from langchain.chains.base import Chain
4
+ from langchain.memory import ConversationBufferMemory
5
+ from langchain.schema import BaseRetriever
6
+ from pydantic import BaseModel, Field
7
+
8
+ from edu_assistant.learning_tasks.base import BaseTask
9
+ from edu_assistant.utils.langchain_utils import load_llm
10
+
11
+ TEMPLATE = """The following is a friendly conversation between a human and an ai.
12
+ The ai is talkative and provides lots of specific details from its context.
13
+ If the ai does not know the answer to a question, it truthfully says it does not know.
14
+ The ai act following below instructions:
15
+ ---
16
+ {instruction}
17
+ ---
18
+
19
+ The coding problem:
20
+ ---
21
+ {problem}
22
+ ---
23
+
24
+ Student's code:
25
+ ---
26
+ {answer}
27
+ ---
28
+
29
+ Current conversation:
30
+ {{history}}
31
+ Human: {{input}}
32
+ AI:"""
33
+
34
+ DEFAULT_INSTRUCTION = """Act as a c++ professional to check student's code.
35
+ The code is written by a student aged 5-10 and mostly like to buggy or bad performanced.
36
+ """
37
+
38
+ DEFAULT_FIRST_QUESTION = "请问这段代码中有什么问题吗?"
39
+
40
+
41
+ class CodingProblem(BaseModel):
42
+ question: str = Field()
43
+ standard_answer: str = Field(default="")
44
+ analysis: str = Field(default="")
45
+ extra: list[str] = Field(default_factory=lambda _: list())
46
+
47
+ # TODO: Add cache to expr function with pydantic 2 computed_field decorator.
48
+ # Wait for langchain to support pydantic2.
49
+
50
+ def expr(self, lang=""):
51
+ expr = f"Question:\n```\n{self.question}\n```\n"
52
+ expr += (
53
+ f"Question Standard Answer (There might be others):\n```{lang}\n{self.standard_answer}```"
54
+ if self.standard_answer
55
+ else ""
56
+ )
57
+ expr += f"Solution Analysis:\n```{self.analysis}```\n" if self.analysis else ""
58
+ expr += "".join(self.extra) + "\n"
59
+ return expr
60
+
61
+ def __str__(self):
62
+ return self.expr()
63
+
64
+
65
+ class CodingAnswer(BaseModel):
66
+ answer: str = Field()
67
+ extra: list[str] = Field(default="")
68
+
69
+ def expr(self, lang=""):
70
+ expr = f"Answer:\n```{lang}\n{self.answer}\n```\n"
71
+ expr += "".join(self.extra) + "\n"
72
+ return expr
73
+
74
+ def __str__(self):
75
+ return self.expr()
76
+
77
+
78
+ class CodingProblemAnalysis(BaseTask):
79
+ def __init__(self, instruction: str = DEFAULT_INSTRUCTION, lang: str = "", knowledge: BaseRetriever = None):
80
+ assert lang in ["python", "cpp", "java", "javascript", "go", "c#", ""]
81
+
82
+ self.lang = lang
83
+ self.instruction = instruction
84
+ self._session_store = {}
85
+ self._knowledge = knowledge
86
+
87
+ def start_analysis(self, problem: CodingProblem, answer: CodingAnswer, first_question: str = None) -> dict:
88
+ """start analysis of a coding problem and incorrect answer.
89
+
90
+ Args:
91
+ problem (CodingProblem): a coding problem
92
+ answer (CodingAnswer): a coding problem answer
93
+
94
+ Returns:
95
+ dict: question answer and metadata
96
+ """
97
+ chain = self._build_chain(problem, answer)
98
+ session_id = self._create_session_id()
99
+ self._session_store[session_id] = chain
100
+
101
+ result = chain({"input": first_question if first_question else DEFAULT_FIRST_QUESTION, "history": ""})
102
+
103
+ result["session_id"] = session_id
104
+
105
+ return result
106
+
107
+ def ask(self, question: str, session_id: str) -> dict:
108
+ """further ask question on a coding problem.
109
+
110
+ Args:
111
+ question (str): question to llm.
112
+ session_id (str): specify a problem and answer session.
113
+
114
+ Returns:
115
+ dict: question answer and metadata
116
+ """
117
+ assert question
118
+
119
+ chain = self._session_store[session_id]
120
+
121
+ result = chain({"input": question})
122
+
123
+ result["session_id"] = session_id
124
+
125
+ return result
126
+
127
+ def _build_chain(self, problem: CodingProblem, answer: CodingAnswer) -> Chain:
128
+ llm = load_llm()
129
+ memory = ConversationBufferMemory()
130
+ prompt = PromptTemplate.from_template(
131
+ TEMPLATE.format(
132
+ instruction=self.instruction, problem=problem.expr(lang=self.lang), answer=answer.expr(lang=self.lang)
133
+ )
134
+ )
135
+
136
+ if not self._knowledge:
137
+ return ConversationChain(
138
+ llm=llm,
139
+ memory=memory,
140
+ prompt=prompt,
141
+ )
142
+ else:
143
+ return ConversationalRetrievalChain.from_llm(
144
+ llm=llm,
145
+ memory=memory,
146
+ retriever=self._knowledge,
147
+ condense_question_llm=llm,
148
+ return_source_documents=True,
149
+ combine_docs_chain_kwargs={"prompt": prompt},
150
+ )
151
+
152
+ @staticmethod
153
+ def build_coding_problem(question: str, standard_answer: str = "", analysis: str = "", extra: list[str] = None):
154
+ extra = [] if extra is None else extra
155
+ return CodingProblem(question=question, standard_answer=standard_answer, analysis=analysis, extra=extra)
156
+
157
+ @staticmethod
158
+ def build_coding_answer(answer: str, extra: list[str] = None):
159
+ extra = [] if extra is None else extra
160
+ return CodingAnswer(answer=answer, extra=extra)
edu_assistant/learning_tasks/qa.py CHANGED
@@ -1,36 +1,47 @@
1
- import random
2
- import sys
3
-
4
- from langchain import PromptTemplate
5
- from langchain.chains import ConversationalRetrievalChain, ConversationChain
 
6
  from langchain.chains.base import Chain
7
  from langchain.memory import ConversationBufferMemory
8
  from langchain.schema import BaseRetriever
9
 
10
  from edu_assistant.learning_tasks.base import BaseTask
11
- from edu_assistant.utils.langchain_utils import load_llm, update_chat_memory
12
 
13
- TEMPLATE = """{instruction}
14
- The following is a friendly conversation between a human and an AI.
15
- The AI is talkative and provides lots of specific details from its context.
16
- If the AI does not know the answer to a question, it truthfully says it does not know.
 
 
 
17
 
18
  Current conversation:
19
  {{history}}
20
  Human: {{input}}
21
  AI:"""
22
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
  class QaTask(BaseTask):
25
- _qa: Chain
26
  _session_store: dict
 
 
27
 
28
- def __init__(
29
- self,
30
- instruction: str = "",
31
- knowledge: BaseRetriever = None,
32
- session_store: dict[int, ConversationBufferMemory] = None,
33
- ):
34
  """Create a new QaTask service.
35
 
36
  Args:
@@ -38,36 +49,66 @@ class QaTask(BaseTask):
38
  knowledge (BaseRetriever, optional): Answer question with this knowledge retriever.
39
  If not set, will not use knowledge to answer question.
40
  Defaults to None.
41
- session_store (dict, optional): External chat history store. Defaults to None.
42
  If not set, will use internal memory to store chat history. Which will be lost after restart and might
43
  cost huge memory.
44
  """
45
- self._prompt = TEMPLATE.format(instruction=instruction)
46
- self._qa = self._build_chain(knowledge)
47
- self._session_store = {} if not session_store else session_store
48
 
49
- def _build_chain(self, knowledge):
50
- if not knowledge:
51
- return ConversationChain(llm=load_llm(), prompt=PromptTemplate.from_template(self._prompt))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  else:
53
  return ConversationalRetrievalChain.from_llm(
54
  llm=load_llm(),
55
- retriever=knowledge,
56
  condense_question_llm=load_llm(),
57
  return_source_documents=True,
58
- combine_docs_chain_kwargs={"prompt": PromptTemplate.from_template(self._prompt)},
 
59
  )
60
 
61
- def ask(self, question: str, session: bool = True, session_id: int = None, session_mem: list | None = None) -> dict:
 
 
 
 
 
 
62
  """ask a question with chat history.
63
 
64
  Args:
65
  question (str): question to llm.
66
  session (bool, optional): whether use and store chat history. Defaults to False.
67
  if session_id is not set, a new session will be created.
68
- session_id (int, optional): specify a history session. Defaults to None.
69
- session_mem (list | None, optional): specify chat history. Defaults to None.
70
- if session_id is also set, chat history will be fully replaced.
71
 
72
  Returns:
73
  dict: question answer and metadata.
@@ -76,41 +117,27 @@ class QaTask(BaseTask):
76
  """
77
 
78
  if session:
79
- session_id = session_id or self._create_session_id()
80
- memory = session_mem or self._get_session_mem(session_id)
 
 
 
 
 
 
 
81
 
82
- result = self._ask(question, memory)
 
83
 
84
- update_chat_memory(memory, question, result["response"])
85
- self._update_session_mem(session_id, memory)
86
- else:
87
- result = self._ask(question)
88
 
89
  if session_id:
90
  result["session_id"] = session_id
91
 
92
  return result
93
 
94
- def _ask(self, question, memory: ConversationBufferMemory = None) -> dict:
95
- if memory is None:
96
- return self._qa({"input": question, "history": ""})
97
- else:
98
- return self._qa({"input": question, "history": memory.chat_memory})
99
-
100
- def _get_session_mem(self, session_id):
101
- if session_id not in self._session_store:
102
- memory = self._init_memory(session_id)
103
- else:
104
- memory = self._session_store.get(session_id)
105
-
106
- return memory
107
-
108
- def _update_session_mem(self, session_id, memory):
109
- self._session_store[session_id] = memory
110
-
111
- def _create_session_id(self):
112
- return random.randint(1, sys.maxsize)
113
-
114
- def _init_memory(self, session_id) -> ConversationBufferMemory:
115
- # TODO: redis memory store
116
- return ConversationBufferMemory(memory_key="chat_history", return_messages=True, output_key="answer")
 
1
+ from langchain import LLMChain, PromptTemplate
2
+ from langchain.chains import (
3
+ ConversationalRetrievalChain,
4
+ ConversationChain,
5
+ RetrievalQA,
6
+ )
7
  from langchain.chains.base import Chain
8
  from langchain.memory import ConversationBufferMemory
9
  from langchain.schema import BaseRetriever
10
 
11
  from edu_assistant.learning_tasks.base import BaseTask
12
+ from edu_assistant.utils.langchain_utils import load_llm
13
 
14
+ TEMPLATE_CHAT = """The following is a friendly conversation between a human and an ai.
15
+ The ai is talkative and provides lots of specific details from its context.
16
+ If the ai does not know the answer to a question, it truthfully says it does not know.
17
+ The ai act following below instructions:
18
+ ---
19
+ {instruction}
20
+ ---
21
 
22
  Current conversation:
23
  {{history}}
24
  Human: {{input}}
25
  AI:"""
26
 
27
+ TEMPLATE_ONCE = """The following is a friendly conversation between a human and an ai.
28
+ The ai is talkative and provides lots of specific details from its context.
29
+ If the ai does not know the answer to a question, it truthfully says it does not know.
30
+ The ai act following below instructions:
31
+ ---
32
+ {instruction}
33
+ ---
34
+
35
+ {{input}}
36
+ """
37
+
38
 
39
  class QaTask(BaseTask):
 
40
  _session_store: dict
41
+ _knowledge: BaseRetriever | None
42
+ _qa_once: Chain
43
 
44
+ def __init__(self, instruction: str = "", knowledge: BaseRetriever = None):
 
 
 
 
 
45
  """Create a new QaTask service.
46
 
47
  Args:
 
49
  knowledge (BaseRetriever, optional): Answer question with this knowledge retriever.
50
  If not set, will not use knowledge to answer question.
51
  Defaults to None.
52
+ session_store (dict, optional): chat history store. Defaults to None.
53
  If not set, will use internal memory to store chat history. Which will be lost after restart and might
54
  cost huge memory.
55
  """
56
+ self._chat_prompt = PromptTemplate.from_template(TEMPLATE_CHAT.format(instruction=instruction))
57
+ self._once_prompt = PromptTemplate.from_template(TEMPLATE_ONCE.format(instruction=instruction))
 
58
 
59
+ self._session_store = {}
60
+ self._knowledge = knowledge
61
+
62
+ self._qa_once = self._build_once_chain()
63
+
64
+ def _build_once_chain(self):
65
+ if not self._knowledge:
66
+ return LLMChain(
67
+ llm=load_llm(),
68
+ prompt=self._once_prompt,
69
+ memory=ConversationBufferMemory(),
70
+ )
71
+ else:
72
+ return RetrievalQA.from_llm(
73
+ llm=load_llm(),
74
+ retriever=self._knowledge,
75
+ return_source_documents=True,
76
+ prompt=self._once_prompt,
77
+ )
78
+
79
+ def _build_chat_chain(self):
80
+ if not self._knowledge:
81
+ return ConversationChain(
82
+ llm=load_llm(),
83
+ memory=ConversationBufferMemory(),
84
+ prompt=self._chat_prompt,
85
+ )
86
  else:
87
  return ConversationalRetrievalChain.from_llm(
88
  llm=load_llm(),
89
+ retriever=self._knowledge,
90
  condense_question_llm=load_llm(),
91
  return_source_documents=True,
92
+ combine_docs_chain_kwargs={"prompt": PromptTemplate.from_template(self._chat_prompt)},
93
+ memory=ConversationBufferMemory(),
94
  )
95
 
96
+ def ask(
97
+ self,
98
+ question: str,
99
+ session: bool = True,
100
+ session_id: str = None,
101
+ session_mem: ConversationBufferMemory | None = None,
102
+ ) -> dict:
103
  """ask a question with chat history.
104
 
105
  Args:
106
  question (str): question to llm.
107
  session (bool, optional): whether use and store chat history. Defaults to False.
108
  if session_id is not set, a new session will be created.
109
+ session_id (str, optional): specify a history qa session. Defaults to None.
110
+ session_mem (list | None, optional): specify session memory. Defaults to None.
111
+ if session_id is also set, memory will be replaced by passed one.
112
 
113
  Returns:
114
  dict: question answer and metadata.
 
117
  """
118
 
119
  if session:
120
+ args = {"input": question}
121
+ if session_id and session_id in self._session_store:
122
+ chain = self._session_store[session_id]
123
+ else:
124
+ session_id = self._create_session_id()
125
+ chain = self._create_session_chain(session_id)
126
+ else:
127
+ args = {"input": question, "history": ""}
128
+ chain = self._qa_once
129
 
130
+ if session_mem:
131
+ chain.memory = session_mem
132
 
133
+ result = chain(args)
 
 
 
134
 
135
  if session_id:
136
  result["session_id"] = session_id
137
 
138
  return result
139
 
140
+ def _create_session_chain(self, session_id) -> ConversationChain:
141
+ chain = self._build_chat_chain()
142
+ self._session_store[session_id] = chain
143
+ return chain
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
edu_assistant/utils/langchain_utils.py CHANGED
@@ -1,10 +1,12 @@
1
  import os
 
2
 
3
  from langchain.chat_models import AzureChatOpenAI, ChatOpenAI
4
  from langchain.chat_models.base import BaseChatModel
5
  from langchain.memory.chat_memory import BaseChatMemory
6
 
7
 
 
8
  def load_llm() -> BaseChatModel:
9
  if os.environ.get("AZURE_OPENAI"):
10
  llm = AzureChatOpenAI(
 
1
  import os
2
+ from functools import lru_cache
3
 
4
  from langchain.chat_models import AzureChatOpenAI, ChatOpenAI
5
  from langchain.chat_models.base import BaseChatModel
6
  from langchain.memory.chat_memory import BaseChatMemory
7
 
8
 
9
+ @lru_cache(maxsize=1)
10
  def load_llm() -> BaseChatModel:
11
  if os.environ.get("AZURE_OPENAI"):
12
  llm = AzureChatOpenAI(
examples/coding_problem.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+
3
+ from edu_assistant.learning_tasks import CodingProblemAnalysis
4
+
5
+ # 创建一个CodingProblem实例
6
+ problem = CodingProblemAnalysis.build_coding_problem(
7
+ question="请编写一个函数,该函数接收一个整数列表,并返回该列表的最大值。",
8
+ standard_answer="def find_max(lst):\n\treturn max(lst)",
9
+ analysis="在这个问题中,我们需要使用Python的内置函数max来找到列表的最大值。",
10
+ )
11
+
12
+ # 创建一个CodingAnswer实例
13
+ answer = CodingProblemAnalysis.build_coding_answer(
14
+ answer="def find_max(lst):\n\treturn lst[0]",
15
+ )
16
+
17
+ analysis = CodingProblemAnalysis(lang="python")
18
+ result = analysis.start_analysis(problem, answer)
19
+ print(json.dumps(result, ensure_ascii=False, indent=4))
20
+
21
+ next_question = "如果改成返回最小值呢?"
22
+ result = analysis.ask(next_question, result["session_id"])
23
+ print(json.dumps(result, ensure_ascii=False, indent=4))
examples/qa.py CHANGED
@@ -19,8 +19,8 @@ def qa_once():
19
 
20
  def qa_twice():
21
  task = QaTask()
22
- task.ask("请问如何释放一个数组?")
23
- result = task.ask("那指针呢?")
24
  print(json.dumps(result, ensure_ascii=False, indent=4))
25
 
26
 
 
19
 
20
  def qa_twice():
21
  task = QaTask()
22
+ result = task.ask("请问如何释放一个数组?")
23
+ result = task.ask("那指针呢?", session_id=result["session_id"])
24
  print(json.dumps(result, ensure_ascii=False, indent=4))
25
 
26
 
poetry.lock CHANGED
@@ -1136,19 +1136,20 @@ reference = "aliyun"
1136
 
1137
  [[package]]
1138
  name = "langchain"
1139
- version = "0.0.179"
1140
  description = "Building applications with LLMs through composability"
1141
  optional = false
1142
  python-versions = ">=3.8.1,<4.0"
1143
  files = [
1144
- {file = "langchain-0.0.179-py3-none-any.whl", hash = "sha256:1af609e32d9297413ca7162efa97f001c8d87217533d3b5ca7cb87b44ddddf35"},
1145
- {file = "langchain-0.0.179.tar.gz", hash = "sha256:9c1ddddf4b24f5c0f981625dd248cf61bcfda7c1ebecf7d780fedd9b36bbc5ae"},
1146
  ]
1147
 
1148
  [package.dependencies]
1149
  aiohttp = ">=3.8.3,<4.0.0"
1150
  async-timeout = {version = ">=4.0.0,<5.0.0", markers = "python_version < \"3.11\""}
1151
  dataclasses-json = ">=0.5.7,<0.6.0"
 
1152
  numexpr = ">=2.8.4,<3.0.0"
1153
  numpy = ">=1,<2"
1154
  openapi-schema-pydantic = ">=1.2,<2.0"
@@ -1159,15 +1160,17 @@ SQLAlchemy = ">=1.4,<3"
1159
  tenacity = ">=8.1.0,<9.0.0"
1160
 
1161
  [package.extras]
1162
- all = ["O365 (>=2.0.26,<3.0.0)", "aleph-alpha-client (>=2.15.0,<3.0.0)", "anthropic (>=0.2.6,<0.3.0)", "arxiv (>=1.4,<2.0)", "atlassian-python-api (>=3.36.0,<4.0.0)", "azure-ai-formrecognizer (>=3.2.1,<4.0.0)", "azure-ai-vision (>=0.11.1b1,<0.12.0)", "azure-cognitiveservices-speech (>=1.28.0,<2.0.0)", "azure-cosmos (>=4.4.0b1,<5.0.0)", "azure-identity (>=1.12.0,<2.0.0)", "beautifulsoup4 (>=4,<5)", "clickhouse-connect (>=0.5.14,<0.6.0)", "cohere (>=3,<4)", "deeplake (>=3.3.0,<4.0.0)", "docarray[hnswlib] (>=0.32.0,<0.33.0)", "duckduckgo-search (>=2.8.6,<3.0.0)", "elasticsearch (>=8,<9)", "faiss-cpu (>=1,<2)", "google-api-python-client (==2.70.0)", "google-search-results (>=2,<3)", "gptcache (>=0.1.7)", "html2text (>=2020.1.16,<2021.0.0)", "huggingface_hub (>=0,<1)", "jina (>=3.14,<4.0)", "jinja2 (>=3,<4)", "jq (>=1.4.1,<2.0.0)", "lancedb (>=0.1,<0.2)", "langkit (>=0.0.1.dev3,<0.1.0)", "lark (>=1.1.5,<2.0.0)", "lxml (>=4.9.2,<5.0.0)", "manifest-ml (>=0.0.1,<0.0.2)", "neo4j (>=5.8.1,<6.0.0)", "networkx (>=2.6.3,<3.0.0)", "nlpcloud (>=1,<2)", "nltk (>=3,<4)", "nomic (>=1.0.43,<2.0.0)", "openai (>=0,<1)", "openlm (>=0.0.5,<0.0.6)", "opensearch-py (>=2.0.0,<3.0.0)", "pdfminer-six (>=20221105,<20221106)", "pexpect (>=4.8.0,<5.0.0)", "pgvector (>=0.1.6,<0.2.0)", "pinecone-client (>=2,<3)", "pinecone-text (>=0.4.2,<0.5.0)", "psycopg2-binary (>=2.9.5,<3.0.0)", "pyowm (>=3.3.0,<4.0.0)", "pypdf (>=3.4.0,<4.0.0)", "pytesseract (>=0.3.10,<0.4.0)", "pyvespa (>=0.33.0,<0.34.0)", "qdrant-client (>=1.1.2,<2.0.0)", "redis (>=4,<5)", "requests-toolbelt (>=1.0.0,<2.0.0)", "sentence-transformers (>=2,<3)", "spacy (>=3,<4)", "steamship (>=2.16.9,<3.0.0)", "tensorflow-text (>=2.11.0,<3.0.0)", "tiktoken (>=0.3.2,<0.4.0)", "torch (>=1,<3)", "transformers (>=4,<5)", "weaviate-client (>=3,<4)", "wikipedia (>=1,<2)", "wolframalpha (==5.0.0)"]
1163
- azure = ["azure-ai-formrecognizer (>=3.2.1,<4.0.0)", "azure-ai-vision (>=0.11.1b1,<0.12.0)", "azure-cognitiveservices-speech (>=1.28.0,<2.0.0)", "azure-core (>=1.26.4,<2.0.0)", "azure-cosmos (>=4.4.0b1,<5.0.0)", "azure-identity (>=1.12.0,<2.0.0)", "openai (>=0,<1)"]
 
1164
  cohere = ["cohere (>=3,<4)"]
1165
  docarray = ["docarray[hnswlib] (>=0.32.0,<0.33.0)"]
1166
  embeddings = ["sentence-transformers (>=2,<3)"]
1167
- extended-testing = ["atlassian-python-api (>=3.36.0,<4.0.0)", "beautifulsoup4 (>=4,<5)", "chardet (>=5.1.0,<6.0.0)", "gql (>=3.4.1,<4.0.0)", "html2text (>=2020.1.16,<2021.0.0)", "jq (>=1.4.1,<2.0.0)", "lxml (>=4.9.2,<5.0.0)", "pandas (>=2.0.1,<3.0.0)", "pdfminer-six (>=20221105,<20221106)", "psychicapi (>=0.2,<0.3)", "pymupdf (>=1.22.3,<2.0.0)", "pypdf (>=3.4.0,<4.0.0)", "pypdfium2 (>=4.10.0,<5.0.0)", "requests-toolbelt (>=1.0.0,<2.0.0)", "telethon (>=1.28.5,<2.0.0)", "tqdm (>=4.48.0)", "zep-python (>=0.25,<0.26)"]
1168
- llms = ["anthropic (>=0.2.6,<0.3.0)", "cohere (>=3,<4)", "huggingface_hub (>=0,<1)", "manifest-ml (>=0.0.1,<0.0.2)", "nlpcloud (>=1,<2)", "openai (>=0,<1)", "openlm (>=0.0.5,<0.0.6)", "torch (>=1,<3)", "transformers (>=4,<5)"]
 
1169
  openai = ["openai (>=0,<1)", "tiktoken (>=0.3.2,<0.4.0)"]
1170
- qdrant = ["qdrant-client (>=1.1.2,<2.0.0)"]
1171
  text-helpers = ["chardet (>=5.1.0,<6.0.0)"]
1172
 
1173
  [package.source]
@@ -1175,6 +1178,26 @@ type = "legacy"
1175
  url = "http://mirrors.aliyun.com/pypi/simple"
1176
  reference = "aliyun"
1177
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1178
  [[package]]
1179
  name = "marshmallow"
1180
  version = "3.19.0"
@@ -2517,4 +2540,4 @@ reference = "aliyun"
2517
  [metadata]
2518
  lock-version = "2.0"
2519
  python-versions = "^3.10"
2520
- content-hash = "bf7411574c4cfde57b9017ce88a88019ca51f05c24b086bb4477e2ff85201b51"
 
1136
 
1137
  [[package]]
1138
  name = "langchain"
1139
+ version = "0.0.234"
1140
  description = "Building applications with LLMs through composability"
1141
  optional = false
1142
  python-versions = ">=3.8.1,<4.0"
1143
  files = [
1144
+ {file = "langchain-0.0.234-py3-none-any.whl", hash = "sha256:a287f0b944fb1b48cc107cedb8c1ad052e0559327c7658ae20e6ce2e8e851122"},
1145
+ {file = "langchain-0.0.234.tar.gz", hash = "sha256:fdb5ba8176497e5bdd7cbb7594125b1149d306f8c9ed31750160271fece356ee"},
1146
  ]
1147
 
1148
  [package.dependencies]
1149
  aiohttp = ">=3.8.3,<4.0.0"
1150
  async-timeout = {version = ">=4.0.0,<5.0.0", markers = "python_version < \"3.11\""}
1151
  dataclasses-json = ">=0.5.7,<0.6.0"
1152
+ langsmith = ">=0.0.5,<0.0.6"
1153
  numexpr = ">=2.8.4,<3.0.0"
1154
  numpy = ">=1,<2"
1155
  openapi-schema-pydantic = ">=1.2,<2.0"
 
1160
  tenacity = ">=8.1.0,<9.0.0"
1161
 
1162
  [package.extras]
1163
+ all = ["O365 (>=2.0.26,<3.0.0)", "aleph-alpha-client (>=2.15.0,<3.0.0)", "anthropic (>=0.3,<0.4)", "arxiv (>=1.4,<2.0)", "atlassian-python-api (>=3.36.0,<4.0.0)", "awadb (>=0.3.3,<0.4.0)", "azure-ai-formrecognizer (>=3.2.1,<4.0.0)", "azure-ai-vision (>=0.11.1b1,<0.12.0)", "azure-cognitiveservices-speech (>=1.28.0,<2.0.0)", "azure-cosmos (>=4.4.0b1,<5.0.0)", "azure-identity (>=1.12.0,<2.0.0)", "beautifulsoup4 (>=4,<5)", "clarifai (>=9.1.0)", "clickhouse-connect (>=0.5.14,<0.6.0)", "cohere (>=3,<4)", "deeplake (>=3.6.8,<4.0.0)", "docarray[hnswlib] (>=0.32.0,<0.33.0)", "duckduckgo-search (>=3.8.3,<4.0.0)", "elasticsearch (>=8,<9)", "esprima (>=4.0.1,<5.0.0)", "faiss-cpu (>=1,<2)", "google-api-python-client (==2.70.0)", "google-auth (>=2.18.1,<3.0.0)", "google-search-results (>=2,<3)", "gptcache (>=0.1.7)", "html2text (>=2020.1.16,<2021.0.0)", "huggingface_hub (>=0,<1)", "jina (>=3.14,<4.0)", "jinja2 (>=3,<4)", "jq (>=1.4.1,<2.0.0)", "lancedb (>=0.1,<0.2)", "langkit (>=0.0.6,<0.1.0)", "lark (>=1.1.5,<2.0.0)", "libdeeplake (>=0.0.60,<0.0.61)", "lxml (>=4.9.2,<5.0.0)", "manifest-ml (>=0.0.1,<0.0.2)", "marqo (>=0.11.0,<0.12.0)", "momento (>=1.5.0,<2.0.0)", "nebula3-python (>=3.4.0,<4.0.0)", "neo4j (>=5.8.1,<6.0.0)", "networkx (>=2.6.3,<3.0.0)", "nlpcloud (>=1,<2)", "nltk (>=3,<4)", "nomic (>=1.0.43,<2.0.0)", "octoai-sdk (>=0.1.1,<0.2.0)", "openai (>=0,<1)", "openlm (>=0.0.5,<0.0.6)", "opensearch-py (>=2.0.0,<3.0.0)", "pdfminer-six (>=20221105,<20221106)", "pexpect (>=4.8.0,<5.0.0)", "pgvector (>=0.1.6,<0.2.0)", "pinecone-client (>=2,<3)", "pinecone-text (>=0.4.2,<0.5.0)", "psycopg2-binary (>=2.9.5,<3.0.0)", "pymongo (>=4.3.3,<5.0.0)", "pyowm (>=3.3.0,<4.0.0)", "pypdf (>=3.4.0,<4.0.0)", "pytesseract (>=0.3.10,<0.4.0)", "pyvespa (>=0.33.0,<0.34.0)", "qdrant-client (>=1.3.1,<2.0.0)", "rdflib (>=6.3.2,<7.0.0)", "redis (>=4,<5)", "requests-toolbelt (>=1.0.0,<2.0.0)", "sentence-transformers (>=2,<3)", "singlestoredb (>=0.7.1,<0.8.0)", "spacy (>=3,<4)", "steamship (>=2.16.9,<3.0.0)", "tensorflow-text (>=2.11.0,<3.0.0)", "tigrisdb (>=1.0.0b6,<2.0.0)", "tiktoken (>=0.3.2,<0.4.0)", "torch (>=1,<3)", "transformers (>=4,<5)", "weaviate-client (>=3,<4)", "wikipedia (>=1,<2)", "wolframalpha (==5.0.0)"]
1164
+ azure = ["azure-ai-formrecognizer (>=3.2.1,<4.0.0)", "azure-ai-vision (>=0.11.1b1,<0.12.0)", "azure-cognitiveservices-speech (>=1.28.0,<2.0.0)", "azure-core (>=1.26.4,<2.0.0)", "azure-cosmos (>=4.4.0b1,<5.0.0)", "azure-identity (>=1.12.0,<2.0.0)", "azure-search-documents (==11.4.0a20230509004)", "openai (>=0,<1)"]
1165
+ clarifai = ["clarifai (>=9.1.0)"]
1166
  cohere = ["cohere (>=3,<4)"]
1167
  docarray = ["docarray[hnswlib] (>=0.32.0,<0.33.0)"]
1168
  embeddings = ["sentence-transformers (>=2,<3)"]
1169
+ extended-testing = ["atlassian-python-api (>=3.36.0,<4.0.0)", "beautifulsoup4 (>=4,<5)", "bibtexparser (>=1.4.0,<2.0.0)", "cassio (>=0.0.7,<0.0.8)", "chardet (>=5.1.0,<6.0.0)", "esprima (>=4.0.1,<5.0.0)", "gql (>=3.4.1,<4.0.0)", "html2text (>=2020.1.16,<2021.0.0)", "jq (>=1.4.1,<2.0.0)", "lxml (>=4.9.2,<5.0.0)", "mwparserfromhell (>=0.6.4,<0.7.0)", "mwxml (>=0.3.3,<0.4.0)", "openai (>=0,<1)", "pandas (>=2.0.1,<3.0.0)", "pdfminer-six (>=20221105,<20221106)", "pgvector (>=0.1.6,<0.2.0)", "psychicapi (>=0.8.0,<0.9.0)", "py-trello (>=0.19.0,<0.20.0)", "pymupdf (>=1.22.3,<2.0.0)", "pypdf (>=3.4.0,<4.0.0)", "pypdfium2 (>=4.10.0,<5.0.0)", "pyspark (>=3.4.0,<4.0.0)", "rapidfuzz (>=3.1.1,<4.0.0)", "requests-toolbelt (>=1.0.0,<2.0.0)", "scikit-learn (>=1.2.2,<2.0.0)", "streamlit (>=1.18.0,<2.0.0)", "sympy (>=1.12,<2.0)", "telethon (>=1.28.5,<2.0.0)", "tqdm (>=4.48.0)", "zep-python (>=0.32)"]
1170
+ javascript = ["esprima (>=4.0.1,<5.0.0)"]
1171
+ llms = ["anthropic (>=0.3,<0.4)", "clarifai (>=9.1.0)", "cohere (>=3,<4)", "huggingface_hub (>=0,<1)", "manifest-ml (>=0.0.1,<0.0.2)", "nlpcloud (>=1,<2)", "openai (>=0,<1)", "openllm (>=0.1.19)", "openlm (>=0.0.5,<0.0.6)", "torch (>=1,<3)", "transformers (>=4,<5)"]
1172
  openai = ["openai (>=0,<1)", "tiktoken (>=0.3.2,<0.4.0)"]
1173
+ qdrant = ["qdrant-client (>=1.3.1,<2.0.0)"]
1174
  text-helpers = ["chardet (>=5.1.0,<6.0.0)"]
1175
 
1176
  [package.source]
 
1178
  url = "http://mirrors.aliyun.com/pypi/simple"
1179
  reference = "aliyun"
1180
 
1181
+ [[package]]
1182
+ name = "langsmith"
1183
+ version = "0.0.5"
1184
+ description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform."
1185
+ optional = false
1186
+ python-versions = ">=3.8.1,<4.0"
1187
+ files = [
1188
+ {file = "langsmith-0.0.5-py3-none-any.whl", hash = "sha256:c9ce19cf7a45d4b9ef74b3133ace4d0583bc992383296d03c05065e8f871e01f"},
1189
+ {file = "langsmith-0.0.5.tar.gz", hash = "sha256:ffad2fc638cfee8c9d27c9eae2fa3c3f9ec423bf443b1dc44cc8184fa34cd6b2"},
1190
+ ]
1191
+
1192
+ [package.dependencies]
1193
+ pydantic = ">=1,<2"
1194
+ requests = ">=2,<3"
1195
+
1196
+ [package.source]
1197
+ type = "legacy"
1198
+ url = "http://mirrors.aliyun.com/pypi/simple"
1199
+ reference = "aliyun"
1200
+
1201
  [[package]]
1202
  name = "marshmallow"
1203
  version = "3.19.0"
 
2540
  [metadata]
2541
  lock-version = "2.0"
2542
  python-versions = "^3.10"
2543
+ content-hash = "8af40fe57d5e45ae818625a895be0c24a0dce9023aaf7bd9c05471a194e7a32f"
pyproject.toml CHANGED
@@ -10,7 +10,7 @@ packages = [{include = "edu_assistant"}]
10
  [tool.poetry.dependencies]
11
  python = "^3.10"
12
  fastapi = "^0.95.1"
13
- langchain = "^0.0.179"
14
  openai = "^0.27.4"
15
  pydantic = "^1.10.7"
16
  tenacity = "^8.2.2"
 
10
  [tool.poetry.dependencies]
11
  python = "^3.10"
12
  fastapi = "^0.95.1"
13
+ langchain = "^0.0.234"
14
  openai = "^0.27.4"
15
  pydantic = "^1.10.7"
16
  tenacity = "^8.2.2"
tests/learning_tasks/test_base_task.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from edu_assistant.learning_tasks.base import BaseTask
2
+
3
+
4
+ def test_create_session_id():
5
+ task = BaseTask()
6
+ id1 = task._create_session_id()
7
+ id2 = task._create_session_id()
8
+
9
+ assert isinstance(id1, str)
10
+ assert isinstance(id2, str)
11
+ assert id1 != id2
tests/learning_tasks/test_coding_problem.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from unittest import TestCase
2
+ from unittest.mock import MagicMock, patch
3
+
4
+ from edu_assistant.learning_tasks import CodingProblemAnalysis
5
+
6
+
7
+ class TestCodingProblemAnalysis(TestCase):
8
+ def setUp(self):
9
+ self.analysis = CodingProblemAnalysis()
10
+
11
+ @patch.object(
12
+ CodingProblemAnalysis,
13
+ "_build_chain",
14
+ return_value=MagicMock(return_value={"response": "Expected Result"}),
15
+ )
16
+ def test_start_analysis(self, mock_chain):
17
+ problem = MagicMock()
18
+ answer = MagicMock()
19
+ result = self.analysis.start_analysis(problem, answer, "First question")
20
+
21
+ self.assertIn("session_id", result)
22
+ self.assertIn("response", result)
23
+ self.assertEqual(result["response"], "Expected Result")
24
+ mock_chain.assert_called_once_with(problem, answer)
25
+
26
+ @patch.object(
27
+ CodingProblemAnalysis,
28
+ "_build_chain",
29
+ return_value=MagicMock(return_value={"response": "Expected Result"}),
30
+ )
31
+ def test_ask(self, mock_chain):
32
+ session_id = self.analysis.start_analysis(MagicMock(), MagicMock(), "First question")["session_id"]
33
+ result = self.analysis.ask("New question", session_id)
34
+ self.assertIn("session_id", result)
35
+ self.assertIn("response", result)
36
+ self.assertEqual(result["response"], "Expected Result")
37
+ self.assertEqual(result["session_id"], session_id)
38
+ self.analysis._session_store[session_id].has_calls(2)
39
+ self.analysis._session_store[session_id].assert_called_with({"input": "New question"})
tests/learning_tasks/test_qa.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from unittest.mock import MagicMock, patch
2
+
3
+ from langchain import PromptTemplate
4
+
5
+ from edu_assistant.learning_tasks import QaTask
6
+ from edu_assistant.learning_tasks.qa import TEMPLATE_CHAT, TEMPLATE_ONCE
7
+
8
+
9
+ @patch.object(QaTask, "_build_once_chain")
10
+ def test_init_without_knowledge(mocked_build_once_chain):
11
+ task = QaTask(instruction="test")
12
+
13
+ assert task._chat_prompt == PromptTemplate.from_template(TEMPLATE_CHAT.format(instruction="test"))
14
+ assert task._once_prompt == PromptTemplate.from_template(TEMPLATE_ONCE.format(instruction="test"))
15
+ assert task._knowledge is None
16
+ mocked_build_once_chain.assert_called_once()
17
+
18
+
19
+ @patch.object(QaTask, "_build_once_chain")
20
+ @patch.object(QaTask, "_create_session_chain")
21
+ def test_ask_with_session(mocked_create_session_chain, mocked_build_once_chain):
22
+ mocked_chain = MagicMock(return_value={"response": "ok"})
23
+ mocked_build_once_chain.return_value = mocked_chain
24
+ mocked_create_session_chain.return_value = mocked_chain
25
+
26
+ task = QaTask(instruction="test")
27
+
28
+ with patch.object(task, "_create_session_id") as mock_create_id:
29
+ mock_create_id.return_value = 123
30
+ result = task.ask("how are you?", session=True)
31
+
32
+ mock_create_id.assert_called_once()
33
+ mocked_create_session_chain.assert_called_once_with(123)
34
+ assert "session_id" in result
35
+ assert result["session_id"] == 123
36
+ assert "response" in result
37
+ assert result["response"] == "ok"
38
+
39
+
40
+ @patch.object(QaTask, "_build_once_chain")
41
+ def test_ask_without_session(mocked_build_once_chain):
42
+ mocked_llm = MagicMock()
43
+ mocked_llm.run.return_value = {"result": "ok"}
44
+ mocked_build_once_chain.return_value = mocked_llm
45
+ task = QaTask(instruction="test")
46
+
47
+ result = task.ask("how are you?", session=False)
48
+
49
+ mocked_build_once_chain.assert_called_once()
50
+ assert "session_id" not in result
tests/unit_tests/learning_tasks/test_qa.py DELETED
@@ -1,53 +0,0 @@
1
- import unittest
2
- from unittest.mock import MagicMock, patch
3
-
4
- from langchain.memory import ConversationBufferMemory
5
-
6
- from edu_assistant.learning_tasks import QaTask
7
-
8
-
9
- class TestQaTask(unittest.TestCase):
10
- @patch.object(QaTask, "_build_chain")
11
- def test_init(self, mock_build_chain):
12
- mock_build_chain.return_value = MagicMock()
13
- QaTask()
14
- mock_build_chain.assert_called_once()
15
-
16
- @patch.object(QaTask, "_build_chain")
17
- def test_ask_with_session(self, mock_build_chain):
18
- mock_build_chain.return_value = MagicMock()
19
- qa_task = QaTask()
20
- qa_task._ask = MagicMock(return_value={"response": "test answer"})
21
- result = qa_task.ask("test question")
22
- qa_task._ask.assert_called_once()
23
- self.assertEqual(result.get("response"), "test answer")
24
- self.assertIsNotNone(result.get("session_id"))
25
-
26
- @patch.object(QaTask, "_build_chain")
27
- def test_ask_without_session(self, mock_build_chain):
28
- mock_build_chain.return_value = MagicMock()
29
- qa_task = QaTask()
30
- qa_task._ask = MagicMock(return_value={"answer": "test answer"})
31
- result = qa_task.ask("test question", session=False)
32
- qa_task._ask.assert_called_once_with("test question")
33
- self.assertEqual(result, {"answer": "test answer"})
34
-
35
- @patch.object(QaTask, "_build_chain")
36
- def test__get_session_mem(self, mock_build_chain):
37
- mock_build_chain.return_value = MagicMock()
38
- memory = ConversationBufferMemory()
39
- qa_task = QaTask(session_store={1: memory})
40
- result = qa_task._get_session_mem(1)
41
- self.assertEqual(result, memory)
42
-
43
- @patch.object(QaTask, "_build_chain")
44
- def test__update_session_mem(self, mock_build_chain):
45
- mock_build_chain.return_value = MagicMock()
46
- memory = ConversationBufferMemory()
47
- qa_task = QaTask()
48
- qa_task._update_session_mem(1, memory)
49
- self.assertEqual(qa_task._session_store[1], memory)
50
-
51
-
52
- if __name__ == "__main__":
53
- unittest.main()