microsoft/TypeAgent

Public

mirrored from https://github.com/microsoft/TypeAgentAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
285e342fc92f597fd417c23380cd3eb899229f98

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

python/ta/test/cmpsearch.py

210lines · modecode

1# Copyright (c) Microsoft Corporation.
2# Licensed under the MIT License.
3
4import argparse
5import asyncio
6import builtins
7from dataclasses import dataclass
8import json
9
10import numpy as np
11import typechat
12
13from typeagent.knowpro.importing import ConversationSettings
14from typeagent.aitools import auth
15from typeagent.aitools.embeddings import AsyncEmbeddingModel
16from typeagent.demo import ui # TODO: Move what we import to a more appropriate place
17from typeagent.knowpro.convknowledge import create_typechat_model
18from typeagent.knowpro.interfaces import IConversation
19from typeagent.knowpro.search_query_schema import SearchQuery
20from typeagent.knowpro import searchlang
21from typeagent.podcasts.podcast import Podcast
22
23
24@dataclass
25class Context:
26 conversation: IConversation
27 query_translator: typechat.TypeChatJsonTranslator[SearchQuery]
28 embedding_model: AsyncEmbeddingModel
29 options: None
30 interactive: bool
31
32
33def main():
34 # Parse arguments.
35
36 default_qafile = (
37 "../../../AISystems-Archive/data/knowpro/test/Episode_53_Answer_results.json"
38 )
39 default_podcast_file = "testdata/Episode_53_AdrianTchaikovsky_index"
40
41 explanation = "a list of objects with 'question' and 'answer' keys"
42 parser = argparse.ArgumentParser(description="Parse Q/A data file")
43 parser.add_argument(
44 "--qafile",
45 type=str,
46 default=default_qafile,
47 help=f"Path to the data file ({explanation})",
48 )
49 parser.add_argument(
50 "--podcast",
51 type=str,
52 default=default_podcast_file,
53 help="Path to the podcast index files (excluding the '_index.json' suffix)",
54 )
55 parser.add_argument(
56 "--skip",
57 type=int,
58 default=0,
59 help="Number of initial Q/A pairs to skip (for debugging purposes)",
60 )
61 parser.add_argument(
62 "--interactive",
63 "-i",
64 action="store_true",
65 default=False,
66 help="Run in interactive mode, waiting for user input before each question",
67 )
68 args = parser.parse_args()
69
70 # Read evaluation data.
71
72 with open(args.qafile, "r") as file:
73 data = json.load(file)
74 assert isinstance(data, list), "Expected a list of Q/A pairs"
75 assert len(data) > 0, "Expected non-empty Q/A data"
76 assert all(
77 isinstance(qa_pair, dict) and "question" in qa_pair and "answer" in qa_pair
78 for qa_pair in data
79 ), "Expected each Q/A pair to be a dict with 'question' and 'answer' keys"
80
81 # Read podcast data.
82
83 auth.load_dotenv()
84 settings = ConversationSettings()
85 with ui.timelog("Loading podcast data"):
86 conversation = Podcast.read_from_file(args.podcast, settings)
87 assert conversation is not None, f"Failed to load podcast from {file!r}"
88
89 # Create translator.
90
91 model = create_typechat_model()
92 query_translator = ui.create_translator(model, SearchQuery)
93
94 # Create context.
95
96 context = Context(
97 conversation,
98 query_translator,
99 AsyncEmbeddingModel(),
100 options=None, # TODO: Set options if needed
101 interactive=args.interactive,
102 )
103
104 # Loop over eval data, skipping duplicate questions
105 # (Those differ in 'cmd' value, which we don't support yet.)
106
107 skip = args.skip
108 last_q = ""
109 counter = 0
110 for qa_pair in data:
111 counter += 1
112 question = qa_pair.get("question")
113 answer = qa_pair.get("answer")
114 if not (question and answer) or question == last_q:
115 continue
116 last_q = question
117 if skip > 0:
118 skip -= 1
119 continue
120
121 # Wait for user input before continuing.
122 if context.interactive:
123 try:
124 input("Press Enter to continue... ")
125 except (EOFError, KeyboardInterrupt):
126 print()
127 break
128
129 # Compare the given answer with the actual answer for the question.
130 actual_answer, score = asyncio.run(compare(context, qa_pair))
131 print("-" * 25, counter, "-" * 25)
132 if not context.interactive and score < 0.97:
133 print(f"Question: {question}")
134 print(f"Expected answer:\n{answer}")
135 print("-" * 20)
136 print(f"Actual answer:\n{actual_answer}")
137 print(f"Score: {score:.3f}")
138 else:
139 print(f"Score: {score:.3f} (question: {question})")
140
141
142async def compare(
143 context: Context, qa_pair: dict[str, str | None]
144) -> tuple[str | None, float]:
145 the_answer: str | None = None
146 score = 0.0
147
148 question = qa_pair.get("question")
149 answer = qa_pair.get("answer")
150 cmd = qa_pair.get("cmd")
151 if not (question and answer):
152 return None, score
153
154 if not context.interactive:
155 print = lambda *args, **kwds: None # Disable printing in non-interactive mode
156 else:
157 print = builtins.print
158
159 print()
160 print("=" * 40)
161 if cmd:
162 print(f"Command: {cmd}")
163 print(f"Question: {question}")
164 print(f"Answer: {answer}")
165 print("-" * 40)
166
167 result = await searchlang.search_conversation_with_language(
168 context.conversation,
169 context.query_translator,
170 question,
171 context.options,
172 )
173 print("-" * 40)
174 if not isinstance(result, typechat.Success):
175 print("Error:", result.message)
176 else:
177 all_answers, combined_answer = await ui.generate_answers(
178 result.value, context.conversation, question
179 )
180 print("-" * 40)
181 if combined_answer.type == "NoAnswer":
182 print("Failure:", combined_answer.whyNoAnswer)
183 print("All answers:")
184 if context.interactive:
185 ui.pretty_print(all_answers)
186 else:
187 assert combined_answer.answer is not None, "Expected an answer"
188 the_answer = combined_answer.answer
189 score = await equality_score(context, answer, the_answer)
190 print(the_answer)
191 print("Correctness score:", score)
192 print("=" * 40)
193
194 return the_answer, score
195
196
197async def equality_score(context: Context, a: str, b: str) -> float:
198 a = a.strip()
199 b = b.strip()
200 if a == b:
201 return 1.0
202 if a.lower() == b.lower():
203 return 0.999
204 embeddings = await context.embedding_model.get_embeddings([a, b])
205 assert embeddings.shape[0] == 2, "Expected two embeddings"
206 return np.dot(embeddings[0], embeddings[1])
207
208
209if __name__ == "__main__":
210 main()
211