pycityagent 1.0.0__py3-none-any.whl → 2.0.0a2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pycityagent/__init__.py +7 -3
- pycityagent/agent.py +180 -284
- pycityagent/economy/__init__.py +5 -0
- pycityagent/economy/econ_client.py +307 -0
- pycityagent/environment/__init__.py +7 -0
- pycityagent/environment/interact/interact.py +141 -0
- pycityagent/environment/sence/__init__.py +0 -0
- pycityagent/{brain → environment/sence}/static.py +1 -1
- pycityagent/environment/sidecar/__init__.py +8 -0
- pycityagent/environment/sidecar/sidecarv2.py +109 -0
- pycityagent/environment/sim/__init__.py +29 -0
- pycityagent/environment/sim/aoi_service.py +38 -0
- pycityagent/environment/sim/client.py +126 -0
- pycityagent/environment/sim/clock_service.py +43 -0
- pycityagent/environment/sim/economy_services.py +191 -0
- pycityagent/environment/sim/lane_service.py +110 -0
- pycityagent/environment/sim/light_service.py +120 -0
- pycityagent/environment/sim/person_service.py +294 -0
- pycityagent/environment/sim/road_service.py +38 -0
- pycityagent/environment/sim/sim_env.py +145 -0
- pycityagent/environment/sim/social_service.py +58 -0
- pycityagent/environment/simulator.py +320 -0
- pycityagent/environment/utils/__init__.py +10 -0
- pycityagent/environment/utils/base64.py +16 -0
- pycityagent/environment/utils/const.py +242 -0
- pycityagent/environment/utils/geojson.py +26 -0
- pycityagent/environment/utils/grpc.py +57 -0
- pycityagent/environment/utils/map_utils.py +157 -0
- pycityagent/environment/utils/port.py +11 -0
- pycityagent/environment/utils/protobuf.py +39 -0
- pycityagent/llm/__init__.py +6 -0
- pycityagent/llm/embedding.py +136 -0
- pycityagent/llm/llm.py +430 -0
- pycityagent/llm/llmconfig.py +15 -0
- pycityagent/llm/utils.py +6 -0
- pycityagent/memory/__init__.py +11 -0
- pycityagent/memory/const.py +41 -0
- pycityagent/memory/memory.py +453 -0
- pycityagent/memory/memory_base.py +168 -0
- pycityagent/memory/profile.py +165 -0
- pycityagent/memory/self_define.py +165 -0
- pycityagent/memory/state.py +173 -0
- pycityagent/memory/utils.py +27 -0
- pycityagent/message/__init__.py +0 -0
- pycityagent/simulation/__init__.py +7 -0
- pycityagent/simulation/interview.py +36 -0
- pycityagent/simulation/simulation.py +352 -0
- pycityagent/simulation/survey/__init__.py +9 -0
- pycityagent/simulation/survey/manager.py +67 -0
- pycityagent/simulation/survey/models.py +49 -0
- pycityagent/simulation/ui/__init__.py +3 -0
- pycityagent/simulation/ui/interface.py +602 -0
- pycityagent/utils/__init__.py +0 -0
- pycityagent/utils/decorators.py +89 -0
- pycityagent/utils/parsers/__init__.py +12 -0
- pycityagent/utils/parsers/code_block_parser.py +37 -0
- pycityagent/utils/parsers/json_parser.py +86 -0
- pycityagent/utils/parsers/parser_base.py +60 -0
- pycityagent/workflow/__init__.py +24 -0
- pycityagent/workflow/block.py +164 -0
- pycityagent/workflow/prompt.py +72 -0
- pycityagent/workflow/tool.py +246 -0
- pycityagent/workflow/trigger.py +150 -0
- pycityagent-2.0.0a2.dist-info/METADATA +208 -0
- pycityagent-2.0.0a2.dist-info/RECORD +69 -0
- {pycityagent-1.0.0.dist-info → pycityagent-2.0.0a2.dist-info}/WHEEL +1 -2
- pycityagent/ac/__init__.py +0 -6
- pycityagent/ac/ac.py +0 -50
- pycityagent/ac/action.py +0 -14
- pycityagent/ac/controled.py +0 -13
- pycityagent/ac/converse.py +0 -31
- pycityagent/ac/idle.py +0 -17
- pycityagent/ac/shop.py +0 -80
- pycityagent/ac/trip.py +0 -37
- pycityagent/brain/__init__.py +0 -10
- pycityagent/brain/brain.py +0 -52
- pycityagent/brain/brainfc.py +0 -10
- pycityagent/brain/memory.py +0 -541
- pycityagent/brain/persistence/social.py +0 -1
- pycityagent/brain/persistence/spatial.py +0 -14
- pycityagent/brain/reason/shop.py +0 -37
- pycityagent/brain/reason/social.py +0 -148
- pycityagent/brain/reason/trip.py +0 -67
- pycityagent/brain/reason/user.py +0 -122
- pycityagent/brain/retrive/social.py +0 -6
- pycityagent/brain/scheduler.py +0 -408
- pycityagent/brain/sence.py +0 -375
- pycityagent/cc/__init__.py +0 -5
- pycityagent/cc/cc.py +0 -102
- pycityagent/cc/conve.py +0 -6
- pycityagent/cc/idle.py +0 -20
- pycityagent/cc/shop.py +0 -6
- pycityagent/cc/trip.py +0 -13
- pycityagent/cc/user.py +0 -13
- pycityagent/hubconnector/__init__.py +0 -3
- pycityagent/hubconnector/hubconnector.py +0 -137
- pycityagent/image/__init__.py +0 -3
- pycityagent/image/image.py +0 -158
- pycityagent/simulator.py +0 -161
- pycityagent/st/__init__.py +0 -4
- pycityagent/st/st.py +0 -96
- pycityagent/urbanllm/__init__.py +0 -3
- pycityagent/urbanllm/urbanllm.py +0 -132
- pycityagent-1.0.0.dist-info/LICENSE +0 -21
- pycityagent-1.0.0.dist-info/METADATA +0 -181
- pycityagent-1.0.0.dist-info/RECORD +0 -48
- pycityagent-1.0.0.dist-info/top_level.txt +0 -1
- /pycityagent/{brain/persistence/__init__.py → config.py} +0 -0
- /pycityagent/{brain/reason → environment/interact}/__init__.py +0 -0
- /pycityagent/{brain/retrive → environment/message}/__init__.py +0 -0
@@ -0,0 +1,352 @@
|
|
1
|
+
import asyncio
|
2
|
+
import json
|
3
|
+
import logging
|
4
|
+
from datetime import datetime
|
5
|
+
import random
|
6
|
+
from typing import Dict, List, Optional, Callable
|
7
|
+
from mosstool.map._map_util.const import AOI_START_ID
|
8
|
+
|
9
|
+
from pycityagent.llm.llm import LLM
|
10
|
+
from pycityagent.memory.memory import Memory
|
11
|
+
|
12
|
+
from ..agent import Agent
|
13
|
+
from ..environment import Simulator
|
14
|
+
from .interview import InterviewManager
|
15
|
+
from .survey import QuestionType, SurveyManager
|
16
|
+
from .ui import InterviewUI
|
17
|
+
|
18
|
+
logger = logging.getLogger(__name__)
|
19
|
+
|
20
|
+
|
21
|
+
class AgentSimulation:
|
22
|
+
"""城市智能体模拟器"""
|
23
|
+
def __init__(self, agent_class: type[Agent], simulator: Simulator, llm: LLM, agent_prefix: str = "agent_"):
|
24
|
+
"""
|
25
|
+
Args:
|
26
|
+
agent_class: 智能体类
|
27
|
+
simulator: 模拟器
|
28
|
+
llm: 语言模型
|
29
|
+
agent_prefix: 智能体名称前缀
|
30
|
+
"""
|
31
|
+
self.agent_class = agent_class
|
32
|
+
self.simulator = simulator
|
33
|
+
self.llm = llm
|
34
|
+
self.agent_prefix = agent_prefix
|
35
|
+
self._agents: Dict[str, Agent] = {}
|
36
|
+
self._interview_manager = InterviewManager()
|
37
|
+
self._interview_lock = asyncio.Lock()
|
38
|
+
self._start_time = datetime.now()
|
39
|
+
self._agent_run_times: Dict[str, datetime] = {} # 记录每个智能体的运行开始时间
|
40
|
+
self._ui: Optional[InterviewUI] = None
|
41
|
+
self._loop = asyncio.get_event_loop()
|
42
|
+
self._blocked_agents: List[str] = [] # 新增:持续阻塞的智能体列表
|
43
|
+
self._survey_manager = SurveyManager()
|
44
|
+
|
45
|
+
def init_agents(self, agent_count: int, memory_config_func: Callable = None) -> None:
|
46
|
+
"""初始化智能体
|
47
|
+
|
48
|
+
Args:
|
49
|
+
agent_count: 要创建的智能体数量
|
50
|
+
memory_config_func: 返回Memory配置的函数,需要返回(EXTRA_ATTRIBUTES, PROFILE, BASE)元组
|
51
|
+
"""
|
52
|
+
if memory_config_func is None:
|
53
|
+
memory_config_func = self.default_memory_config_func
|
54
|
+
|
55
|
+
for i in range(agent_count):
|
56
|
+
agent_name = f"{self.agent_prefix}{i}"
|
57
|
+
|
58
|
+
# 获取Memory配置
|
59
|
+
extra_attributes, profile, base = memory_config_func()
|
60
|
+
memory = Memory(
|
61
|
+
config=extra_attributes,
|
62
|
+
profile=profile.copy(),
|
63
|
+
base=base.copy()
|
64
|
+
)
|
65
|
+
|
66
|
+
# 创建智能体时传入Memory配置
|
67
|
+
agent = self.agent_class(
|
68
|
+
name=agent_name,
|
69
|
+
simulator=self.simulator,
|
70
|
+
llm=self.llm,
|
71
|
+
memory=memory
|
72
|
+
)
|
73
|
+
|
74
|
+
self.add_agent(agent)
|
75
|
+
|
76
|
+
def default_memory_config_func(self):
|
77
|
+
"""默认的Memory配置函数"""
|
78
|
+
EXTRA_ATTRIBUTES = {
|
79
|
+
# 需求信息
|
80
|
+
"needs": (dict, {
|
81
|
+
'hungry': random.random(), # 饥饿感
|
82
|
+
'tired': random.random(), # 疲劳感
|
83
|
+
'safe': random.random(), # 安全需
|
84
|
+
'social': random.random(), # 社会需求
|
85
|
+
}, True),
|
86
|
+
"current_need": (str, "none", True),
|
87
|
+
"current_plan": (list, [], True),
|
88
|
+
"current_step": (dict, {"intention": "", "type": ""}, True),
|
89
|
+
"execution_context" : (dict, {}, True),
|
90
|
+
"plan_history": (list, [], True),
|
91
|
+
}
|
92
|
+
|
93
|
+
PROFILE = {
|
94
|
+
"gender": random.choice(["male", "female"]),
|
95
|
+
"education": random.choice(["Doctor", "Master", "Bachelor", "College", "High School"]),
|
96
|
+
"consumption": random.choice(["sightly low", "low", "medium", "high"]),
|
97
|
+
"occupation": random.choice(["Student", "Teacher", "Doctor", "Engineer", "Manager", "Businessman", "Artist", "Athlete", "Other"]),
|
98
|
+
"age": random.randint(18, 65),
|
99
|
+
"skill": random.choice(["Good at problem-solving", "Good at communication", "Good at creativity", "Good at teamwork", "Other"]),
|
100
|
+
"family_consumption": random.choice(["low", "medium", "high"]),
|
101
|
+
"personality": random.choice(["outgoint", "introvert", "ambivert", "extrovert"]),
|
102
|
+
"income": random.randint(1000, 10000),
|
103
|
+
"residence": random.choice(["city", "suburb", "rural"]),
|
104
|
+
"race": random.choice(["Chinese", "American", "British", "French", "German", "Japanese", "Korean", "Russian", "Other"]),
|
105
|
+
"religion": random.choice(["none", "Christian", "Muslim", "Buddhist", "Hindu", "Other"]),
|
106
|
+
"marital_status": random.choice(["not married", "married", "divorced", "widowed"]),
|
107
|
+
}
|
108
|
+
|
109
|
+
aois = self.simulator.aois.keys()
|
110
|
+
BASE = {
|
111
|
+
"home": {"aoi_position": {"aoi_id": random.choice(aois)}},
|
112
|
+
"work": {"aoi_position": {"aoi_id": random.choice(aois)}},
|
113
|
+
}
|
114
|
+
|
115
|
+
return EXTRA_ATTRIBUTES, PROFILE, BASE
|
116
|
+
|
117
|
+
def get_agent_runtime(self, agent_name: str) -> str:
|
118
|
+
"""获取智能体运行时间"""
|
119
|
+
if agent_name not in self._agent_run_times:
|
120
|
+
return "-"
|
121
|
+
delta = datetime.now() - self._agent_run_times[agent_name]
|
122
|
+
hours = delta.seconds // 3600
|
123
|
+
minutes = (delta.seconds % 3600) // 60
|
124
|
+
seconds = delta.seconds % 60
|
125
|
+
return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
|
126
|
+
|
127
|
+
def get_total_runtime(self) -> str:
|
128
|
+
"""获取总运行时间"""
|
129
|
+
delta = datetime.now() - self._start_time
|
130
|
+
hours = delta.seconds // 3600
|
131
|
+
minutes = (delta.seconds % 3600) // 60
|
132
|
+
seconds = delta.seconds % 60
|
133
|
+
return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
|
134
|
+
|
135
|
+
def export_chat_history(self, agent_name: str | None) -> str:
|
136
|
+
"""导出对话历史
|
137
|
+
|
138
|
+
Args:
|
139
|
+
agent_name: 可选的智能体名称,如果提供则只导出该智能体的对话
|
140
|
+
|
141
|
+
Returns:
|
142
|
+
str: JSON格式的对话历史
|
143
|
+
"""
|
144
|
+
history = (
|
145
|
+
self._interview_manager.get_agent_history(agent_name)
|
146
|
+
if agent_name
|
147
|
+
else self._interview_manager.get_recent_history(limit=1000)
|
148
|
+
)
|
149
|
+
|
150
|
+
# 转换为易读格式
|
151
|
+
formatted_history = []
|
152
|
+
for record in history:
|
153
|
+
formatted_history.append(
|
154
|
+
{
|
155
|
+
"timestamp": record.timestamp.strftime("%Y-%m-%d %H:%M:%S"),
|
156
|
+
"agent": record.agent_name,
|
157
|
+
"question": record.question,
|
158
|
+
"response": record.response,
|
159
|
+
"blocking": record.blocking,
|
160
|
+
}
|
161
|
+
)
|
162
|
+
|
163
|
+
return json.dumps(formatted_history, ensure_ascii=False, indent=2)
|
164
|
+
|
165
|
+
def toggle_agent_block(self, agent_name: str, blocking: bool) -> str:
|
166
|
+
"""切换智能体的阻塞状态
|
167
|
+
|
168
|
+
Args:
|
169
|
+
agent_name: 能体名称
|
170
|
+
blocking: True表示阻塞,False表示取消阻塞
|
171
|
+
|
172
|
+
Returns:
|
173
|
+
str: 状态变更消息
|
174
|
+
"""
|
175
|
+
if agent_name not in self._agents:
|
176
|
+
return f"找不到智能体 {agent_name}"
|
177
|
+
|
178
|
+
if blocking and agent_name not in self._blocked_agents:
|
179
|
+
self._blocked_agents.append(agent_name)
|
180
|
+
return f"已阻塞智能体 {agent_name}"
|
181
|
+
elif not blocking and agent_name in self._blocked_agents:
|
182
|
+
self._blocked_agents.remove(agent_name)
|
183
|
+
return f"已取消阻塞智能体 {agent_name}"
|
184
|
+
|
185
|
+
return f"智能体 {agent_name} 状态未变"
|
186
|
+
|
187
|
+
async def interview_agent(self, agent_name: str, question: str) -> str:
|
188
|
+
"""采访指定智能体"""
|
189
|
+
agent = self._agents.get(agent_name)
|
190
|
+
if not agent:
|
191
|
+
return "找不到指定的智能体"
|
192
|
+
|
193
|
+
try:
|
194
|
+
response = await agent.generate_response(question)
|
195
|
+
# 记录采访历史
|
196
|
+
self._interview_manager.add_record(
|
197
|
+
agent_name,
|
198
|
+
question,
|
199
|
+
response,
|
200
|
+
blocking=(agent_name in self._blocked_agents),
|
201
|
+
)
|
202
|
+
return response
|
203
|
+
|
204
|
+
except Exception as e:
|
205
|
+
logger.error(f"采访过程出错: {str(e)}")
|
206
|
+
return f"采访过程出现错误: {str(e)}"
|
207
|
+
|
208
|
+
async def submit_survey(self, agent_name: str, survey_id: str) -> str:
|
209
|
+
"""向智能体提交问卷
|
210
|
+
|
211
|
+
Args:
|
212
|
+
agent_name: 智能体名称
|
213
|
+
survey_id: 问卷ID
|
214
|
+
|
215
|
+
Returns:
|
216
|
+
str: 处理结果
|
217
|
+
"""
|
218
|
+
agent = self._agents.get(agent_name)
|
219
|
+
if not agent:
|
220
|
+
return "找不到指定的智能体"
|
221
|
+
|
222
|
+
survey = self._survey_manager.get_survey(survey_id)
|
223
|
+
if not survey:
|
224
|
+
return "找不到指定的问卷"
|
225
|
+
|
226
|
+
try:
|
227
|
+
# 建问卷提示
|
228
|
+
prompt = f"""请以第一人称回答以下调查问卷:
|
229
|
+
|
230
|
+
问卷标题: {survey.title}
|
231
|
+
问卷说明: {survey.description}
|
232
|
+
|
233
|
+
"""
|
234
|
+
for i, question in enumerate(survey.questions):
|
235
|
+
prompt += f"\n问题{i+1}. {question.content}"
|
236
|
+
if question.type in (
|
237
|
+
QuestionType.SINGLE_CHOICE,
|
238
|
+
QuestionType.MULTIPLE_CHOICE,
|
239
|
+
):
|
240
|
+
prompt += "\n选项: " + ", ".join(question.options)
|
241
|
+
elif question.type == QuestionType.RATING:
|
242
|
+
prompt += (
|
243
|
+
f"\n(请给出{question.min_rating}-{question.max_rating}的评分)"
|
244
|
+
)
|
245
|
+
elif question.type == QuestionType.LIKERT:
|
246
|
+
prompt += "\n(1-强烈不同意, 2-不同意, 3-中立, 4-同意, 5-强烈同意)"
|
247
|
+
|
248
|
+
# 生成回答
|
249
|
+
response = await agent.generate_response(prompt)
|
250
|
+
|
251
|
+
# 存储原始回答
|
252
|
+
self._survey_manager.add_response(
|
253
|
+
survey_id, agent_name, {"raw_response": response, "parsed": False}
|
254
|
+
)
|
255
|
+
|
256
|
+
return response
|
257
|
+
|
258
|
+
except Exception as e:
|
259
|
+
logger.error(f"问卷处理出错: {str(e)}")
|
260
|
+
return f"问卷处理出现错误: {str(e)}"
|
261
|
+
|
262
|
+
def create_survey(self, **survey_data: dict) -> None:
|
263
|
+
"""创建新问卷
|
264
|
+
|
265
|
+
Args:
|
266
|
+
survey_data: 问卷数据,包含 title, description, questions
|
267
|
+
|
268
|
+
Returns:
|
269
|
+
更新后的问卷列表
|
270
|
+
"""
|
271
|
+
self._survey_manager.create_survey(**survey_data) # type:ignore
|
272
|
+
|
273
|
+
def get_surveys(self) -> list:
|
274
|
+
"""获取所有问卷"""
|
275
|
+
return self._survey_manager.get_all_surveys()
|
276
|
+
|
277
|
+
def get_survey_questions(self, survey_id: str) -> dict | None:
|
278
|
+
"""获取指定问卷的问题列表
|
279
|
+
|
280
|
+
Args:
|
281
|
+
survey_id: 问卷ID
|
282
|
+
|
283
|
+
Returns:
|
284
|
+
问卷数据,包含 title, description, questions
|
285
|
+
"""
|
286
|
+
for _, survey in self._survey_manager._surveys.items():
|
287
|
+
survey_dict = survey.to_dict()
|
288
|
+
if survey_dict["id"] == survey_id:
|
289
|
+
return survey_dict
|
290
|
+
return None
|
291
|
+
|
292
|
+
async def init_ui(
|
293
|
+
self,
|
294
|
+
server_name: str = "127.0.0.1",
|
295
|
+
server_port: int = 7860,
|
296
|
+
):
|
297
|
+
"""初始化UI"""
|
298
|
+
self._interview_lock = asyncio.Lock()
|
299
|
+
# 初始化GradioUI
|
300
|
+
self._ui = InterviewUI(self)
|
301
|
+
interface = self._ui.create_interface()
|
302
|
+
interface.queue().launch(
|
303
|
+
server_name=server_name,
|
304
|
+
server_port=server_port,
|
305
|
+
prevent_thread_lock=True,
|
306
|
+
quiet=True,
|
307
|
+
)
|
308
|
+
print(
|
309
|
+
f"Gradio Frontend is running on http://{server_name}:{server_port}"
|
310
|
+
)
|
311
|
+
|
312
|
+
async def step(self):
|
313
|
+
"""运行一步, 即每个智能体执行一次forward"""
|
314
|
+
try:
|
315
|
+
tasks = []
|
316
|
+
for agent in self._agents.values():
|
317
|
+
tasks.append(agent.forward())
|
318
|
+
await asyncio.gather(*tasks)
|
319
|
+
except Exception as e:
|
320
|
+
logger.error(f"运行错误: {str(e)}")
|
321
|
+
raise
|
322
|
+
|
323
|
+
async def run(
|
324
|
+
self,
|
325
|
+
day: int = 1,
|
326
|
+
):
|
327
|
+
"""运行模拟器
|
328
|
+
|
329
|
+
Args:
|
330
|
+
day: 运行天数,默认为1天
|
331
|
+
"""
|
332
|
+
try:
|
333
|
+
# 获取开始时间
|
334
|
+
start_time = self.simulator.GetTime()
|
335
|
+
# 计算结束时间(秒)
|
336
|
+
end_time = start_time + day * 24 * 3600 # 将天数转换为秒
|
337
|
+
|
338
|
+
while True:
|
339
|
+
current_time = self.simulator.GetTime()
|
340
|
+
if current_time >= end_time:
|
341
|
+
break
|
342
|
+
|
343
|
+
tasks = []
|
344
|
+
for agent in self._agents.values():
|
345
|
+
if agent.name not in self._blocked_agents:
|
346
|
+
tasks.append(agent.forward())
|
347
|
+
|
348
|
+
await asyncio.gather(*tasks)
|
349
|
+
|
350
|
+
except Exception as e:
|
351
|
+
logger.error(f"模拟器运行错误: {str(e)}")
|
352
|
+
raise
|
@@ -0,0 +1,67 @@
|
|
1
|
+
from typing import List, Dict, Optional
|
2
|
+
from datetime import datetime
|
3
|
+
import uuid
|
4
|
+
import json
|
5
|
+
from .models import Survey, Question, QuestionType
|
6
|
+
|
7
|
+
class SurveyManager:
|
8
|
+
def __init__(self):
|
9
|
+
self._surveys: Dict[str, Survey] = {}
|
10
|
+
|
11
|
+
def create_survey(self, title: str, description: str, questions: List[dict]) -> Survey:
|
12
|
+
"""创建新问卷"""
|
13
|
+
survey_id = str(uuid.uuid4())
|
14
|
+
|
15
|
+
# 转换问题数据
|
16
|
+
survey_questions = []
|
17
|
+
for q in questions:
|
18
|
+
question = Question(
|
19
|
+
content=q["content"],
|
20
|
+
type=QuestionType(q["type"]),
|
21
|
+
required=q.get("required", True),
|
22
|
+
options=q.get("options", []),
|
23
|
+
min_rating=q.get("min_rating", 1),
|
24
|
+
max_rating=q.get("max_rating", 5)
|
25
|
+
)
|
26
|
+
survey_questions.append(question)
|
27
|
+
|
28
|
+
survey = Survey(
|
29
|
+
id=survey_id,
|
30
|
+
title=title,
|
31
|
+
description=description,
|
32
|
+
questions=survey_questions
|
33
|
+
)
|
34
|
+
|
35
|
+
self._surveys[survey_id] = survey
|
36
|
+
return survey
|
37
|
+
|
38
|
+
def get_survey(self, survey_id: str) -> Optional[Survey]:
|
39
|
+
"""获取指定问卷"""
|
40
|
+
return self._surveys.get(survey_id)
|
41
|
+
|
42
|
+
def get_all_surveys(self) -> List[Survey]:
|
43
|
+
"""获取所有问卷"""
|
44
|
+
return list(self._surveys.values())
|
45
|
+
|
46
|
+
def add_response(self, survey_id: str, agent_name: str, response: dict) -> bool:
|
47
|
+
"""添加问卷回答"""
|
48
|
+
survey = self.get_survey(survey_id)
|
49
|
+
if not survey:
|
50
|
+
return False
|
51
|
+
|
52
|
+
survey.responses[agent_name] = {
|
53
|
+
"timestamp": datetime.now(),
|
54
|
+
**response
|
55
|
+
}
|
56
|
+
return True
|
57
|
+
|
58
|
+
def export_results(self, survey_id: str) -> str:
|
59
|
+
"""导出问卷结果"""
|
60
|
+
survey = self.get_survey(survey_id)
|
61
|
+
if not survey:
|
62
|
+
return json.dumps({"error": "问卷不存在"})
|
63
|
+
|
64
|
+
return json.dumps({
|
65
|
+
"survey": survey.to_dict(),
|
66
|
+
"responses": survey.responses
|
67
|
+
}, ensure_ascii=False, indent=2)
|
@@ -0,0 +1,49 @@
|
|
1
|
+
from dataclasses import dataclass, field
|
2
|
+
from typing import List, Dict, Optional
|
3
|
+
from datetime import datetime
|
4
|
+
from enum import Enum
|
5
|
+
import uuid
|
6
|
+
|
7
|
+
class QuestionType(Enum):
|
8
|
+
TEXT = "文本"
|
9
|
+
SINGLE_CHOICE = "单选"
|
10
|
+
MULTIPLE_CHOICE = "多选"
|
11
|
+
RATING = "评分"
|
12
|
+
LIKERT = "李克特量表"
|
13
|
+
|
14
|
+
@dataclass
|
15
|
+
class Question:
|
16
|
+
content: str
|
17
|
+
type: QuestionType
|
18
|
+
required: bool = True
|
19
|
+
options: List[str] = field(default_factory=list)
|
20
|
+
min_rating: int = 1
|
21
|
+
max_rating: int = 5
|
22
|
+
|
23
|
+
def to_dict(self) -> dict:
|
24
|
+
return {
|
25
|
+
"content": self.content,
|
26
|
+
"type": self.type.value,
|
27
|
+
"required": self.required,
|
28
|
+
"options": self.options,
|
29
|
+
"min_rating": self.min_rating,
|
30
|
+
"max_rating": self.max_rating
|
31
|
+
}
|
32
|
+
|
33
|
+
@dataclass
|
34
|
+
class Survey:
|
35
|
+
id: str
|
36
|
+
title: str
|
37
|
+
description: str
|
38
|
+
questions: List[Question]
|
39
|
+
responses: Dict[str, dict] = field(default_factory=dict)
|
40
|
+
created_at: datetime = field(default_factory=datetime.now)
|
41
|
+
|
42
|
+
def to_dict(self) -> dict:
|
43
|
+
return {
|
44
|
+
"id": self.id,
|
45
|
+
"title": self.title,
|
46
|
+
"description": self.description,
|
47
|
+
"questions": [q.to_dict() for q in self.questions],
|
48
|
+
"response_count": len(self.responses)
|
49
|
+
}
|