Spaces:
Runtime error
Runtime error
feat(task): :sparkles: analyze oj wrong answer (#10)
Browse filesAccept a coding problem and incorrect answer as input. Start a conversation to help student figure out whats wrong
- edu_assistant/learning_tasks/__init__.py +2 -1
- edu_assistant/learning_tasks/base.py +5 -1
- edu_assistant/learning_tasks/coding_problem.py +160 -0
- edu_assistant/learning_tasks/qa.py +87 -60
- edu_assistant/utils/langchain_utils.py +2 -0
- examples/coding_problem.py +23 -0
- examples/qa.py +2 -2
- poetry.lock +32 -9
- pyproject.toml +1 -1
- tests/learning_tasks/test_base_task.py +11 -0
- tests/learning_tasks/test_coding_problem.py +39 -0
- tests/learning_tasks/test_qa.py +50 -0
- tests/unit_tests/learning_tasks/test_qa.py +0 -53
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 |
-
|
|
|
|
|
|
| 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
|
| 2 |
-
import
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
|
|
|
| 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
|
| 12 |
|
| 13 |
-
|
| 14 |
-
The
|
| 15 |
-
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
| 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):
|
| 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.
|
| 46 |
-
self.
|
| 47 |
-
self._session_store = {} if not session_store else session_store
|
| 48 |
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
else:
|
| 53 |
return ConversationalRetrievalChain.from_llm(
|
| 54 |
llm=load_llm(),
|
| 55 |
-
retriever=
|
| 56 |
condense_question_llm=load_llm(),
|
| 57 |
return_source_documents=True,
|
| 58 |
-
combine_docs_chain_kwargs={"prompt": PromptTemplate.from_template(self.
|
|
|
|
| 59 |
)
|
| 60 |
|
| 61 |
-
def ask(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 (
|
| 69 |
-
session_mem (list | None, optional): specify
|
| 70 |
-
if session_id is also set,
|
| 71 |
|
| 72 |
Returns:
|
| 73 |
dict: question answer and metadata.
|
|
@@ -76,41 +117,27 @@ class QaTask(BaseTask):
|
|
| 76 |
"""
|
| 77 |
|
| 78 |
if session:
|
| 79 |
-
|
| 80 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
|
| 82 |
-
|
|
|
|
| 83 |
|
| 84 |
-
|
| 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
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 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.
|
| 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.
|
| 1145 |
-
{file = "langchain-0.0.
|
| 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.
|
| 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)", "
|
| 1168 |
-
|
|
|
|
| 1169 |
openai = ["openai (>=0,<1)", "tiktoken (>=0.3.2,<0.4.0)"]
|
| 1170 |
-
qdrant = ["qdrant-client (>=1.
|
| 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 = "
|
|
|
|
| 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.
|
| 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()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|