pycityagent 1.0.0__py3-none-any.whl → 2.0.0a1__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 +27 -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/social_service.py +58 -0
- pycityagent/environment/simulator.py +369 -0
- pycityagent/environment/utils/__init__.py +8 -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/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 +286 -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 +22 -0
- pycityagent/workflow/block.py +137 -0
- pycityagent/workflow/prompt.py +72 -0
- pycityagent/workflow/tool.py +246 -0
- pycityagent/workflow/trigger.py +66 -0
- pycityagent-2.0.0a1.dist-info/METADATA +208 -0
- pycityagent-2.0.0a1.dist-info/RECORD +65 -0
- {pycityagent-1.0.0.dist-info → pycityagent-2.0.0a1.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,37 @@
|
|
1
|
+
import logging
|
2
|
+
from typing import Any
|
3
|
+
|
4
|
+
from .parser_base import ParserBase
|
5
|
+
|
6
|
+
|
7
|
+
class CodeBlockParser(ParserBase):
|
8
|
+
"""A parser that extracts specific objects from a response string enclosed within specific tags.
|
9
|
+
|
10
|
+
Attributes:
|
11
|
+
tag_start (str): The start tag used to identify the beginning of the object.
|
12
|
+
tag_end (str): The end tag used to identify the end of the object.
|
13
|
+
"""
|
14
|
+
|
15
|
+
tag_start = "```{language_name}"
|
16
|
+
tag_end = "```"
|
17
|
+
|
18
|
+
def __init__(self, language_name: str) -> None:
|
19
|
+
"""Initialize the CodeBlockParser with default tags."""
|
20
|
+
super().__init__()
|
21
|
+
self.language_name = language_name
|
22
|
+
|
23
|
+
def parse(self, response: str) -> str:
|
24
|
+
"""Parse the response string to extract and return a str.
|
25
|
+
|
26
|
+
Parameters:
|
27
|
+
response (str): The response string containing the specified language object.
|
28
|
+
|
29
|
+
Returns:
|
30
|
+
str: The parsed `str` object.
|
31
|
+
"""
|
32
|
+
extract_text = self._extract_text_within_tags(
|
33
|
+
response=response,
|
34
|
+
tag_start=self.tag_start.format(language_name=self.language_name),
|
35
|
+
tag_end=self.tag_end,
|
36
|
+
)
|
37
|
+
return extract_text
|
@@ -0,0 +1,86 @@
|
|
1
|
+
import json
|
2
|
+
import logging
|
3
|
+
from typing import Any, Dict
|
4
|
+
|
5
|
+
from .parser_base import ParserBase
|
6
|
+
|
7
|
+
|
8
|
+
class JsonObjectParser(ParserBase):
|
9
|
+
"""A parser that extracts and parses JSON objects from a response string enclosed within specific tags.
|
10
|
+
|
11
|
+
Attributes:
|
12
|
+
tag_start (str): The start tag used to identify the beginning of the JSON object.
|
13
|
+
tag_end (str): The end tag used to identify the end of the JSON object.
|
14
|
+
"""
|
15
|
+
|
16
|
+
tag_start = "```json"
|
17
|
+
tag_end = "```"
|
18
|
+
|
19
|
+
def __init__(self) -> None:
|
20
|
+
"""Initialize the JsonObjectParser with default tags."""
|
21
|
+
super().__init__()
|
22
|
+
|
23
|
+
def parse(self, response: str) -> Any:
|
24
|
+
"""Parse the response string to extract and return a JSON object.
|
25
|
+
|
26
|
+
Parameters:
|
27
|
+
response (str): The response string containing the JSON object.
|
28
|
+
|
29
|
+
Returns:
|
30
|
+
Any: The parsed JSON object.
|
31
|
+
"""
|
32
|
+
extract_text = self._extract_text_within_tags(
|
33
|
+
response=response,
|
34
|
+
tag_start=self.tag_start,
|
35
|
+
tag_end=self.tag_end,
|
36
|
+
)
|
37
|
+
try:
|
38
|
+
eval_parsed_json = eval(extract_text)
|
39
|
+
return eval_parsed_json
|
40
|
+
except Exception as e:
|
41
|
+
# logging.warning(f"Error when handling text {extract_text} with `eval`")
|
42
|
+
pass
|
43
|
+
try:
|
44
|
+
parsed_json = json.loads(extract_text)
|
45
|
+
return parsed_json
|
46
|
+
except json.decoder.JSONDecodeError as e:
|
47
|
+
raw_response = f"{self.tag_start}{extract_text}{self.tag_end}"
|
48
|
+
raise ValueError(
|
49
|
+
f"The content between {self.tag_start} and {self.tag_end} "
|
50
|
+
f"MUST be a JSON object."
|
51
|
+
f'When parsing "{raw_response}", an error occurred: {e}',
|
52
|
+
) from None
|
53
|
+
|
54
|
+
|
55
|
+
class JsonDictParser(JsonObjectParser):
|
56
|
+
"""A parser that extends JsonObjectParser to ensure the parsed JSON is returned as a dictionary.
|
57
|
+
|
58
|
+
Attributes:
|
59
|
+
tag_start (str): The start tag used to identify the beginning of the JSON object.
|
60
|
+
tag_end (str): The end tag used to identify the end of the JSON object.
|
61
|
+
"""
|
62
|
+
|
63
|
+
tag_start = "```json"
|
64
|
+
tag_end = "```"
|
65
|
+
|
66
|
+
def __init__(self) -> None:
|
67
|
+
"""Initialize the JsonDictParser with default tags."""
|
68
|
+
super().__init__()
|
69
|
+
|
70
|
+
def parse(self, response: str) -> Dict:
|
71
|
+
"""Parse the response string to extract and return a JSON object as a dictionary.
|
72
|
+
|
73
|
+
Parameters:
|
74
|
+
response (str): The response string containing the JSON object.
|
75
|
+
|
76
|
+
Returns:
|
77
|
+
Dict: The parsed JSON object as a dictionary.
|
78
|
+
"""
|
79
|
+
parsed_json = super().parse(response)
|
80
|
+
if not isinstance(parsed_json, dict):
|
81
|
+
# If not a dictionary, raise an error
|
82
|
+
raise ValueError(
|
83
|
+
"A JSON dictionary object is wanted, "
|
84
|
+
f"but got {type(parsed_json)} instead."
|
85
|
+
)
|
86
|
+
return dict(parsed_json)
|
@@ -0,0 +1,60 @@
|
|
1
|
+
"""
|
2
|
+
Base class of parser
|
3
|
+
"""
|
4
|
+
|
5
|
+
import re
|
6
|
+
from abc import ABC, abstractmethod
|
7
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
8
|
+
|
9
|
+
|
10
|
+
class ParserBase(ABC):
|
11
|
+
def __init__(
|
12
|
+
self,
|
13
|
+
) -> None:
|
14
|
+
pass
|
15
|
+
|
16
|
+
@abstractmethod
|
17
|
+
def parse(self, response: str) -> Any:
|
18
|
+
"""
|
19
|
+
Parse the string response returned by the model and convert it into a more manageable form.
|
20
|
+
|
21
|
+
Parameters:
|
22
|
+
response (str): The raw string returned by the model.
|
23
|
+
|
24
|
+
Returns:
|
25
|
+
Any: The converted data, the specific type depends on the parsing result.
|
26
|
+
"""
|
27
|
+
pass
|
28
|
+
|
29
|
+
def _extract_text_within_tags(
|
30
|
+
self, response: str, tag_start: str, tag_end: str
|
31
|
+
) -> str:
|
32
|
+
"""
|
33
|
+
Return the string between the first occurrence of the start tag and the end tag in the response string.
|
34
|
+
|
35
|
+
Parameters:
|
36
|
+
response (str): The response string.
|
37
|
+
tag_start (str): The start tag.
|
38
|
+
tag_end (str): The end tag.
|
39
|
+
|
40
|
+
Returns:
|
41
|
+
str: The string between the start and end tags.
|
42
|
+
"""
|
43
|
+
|
44
|
+
def _extract_substring(
|
45
|
+
text: str, tag_start: str, tag_end: str
|
46
|
+
) -> Union[str, None]:
|
47
|
+
pattern = re.escape(tag_start) + r"(.*?)" + re.escape(tag_end)
|
48
|
+
match = re.search(pattern, text, re.DOTALL)
|
49
|
+
if match:
|
50
|
+
return match.group(1).strip()
|
51
|
+
else:
|
52
|
+
return None
|
53
|
+
|
54
|
+
sub_response = _extract_substring(response, tag_start, tag_end)
|
55
|
+
if sub_response is not None:
|
56
|
+
return sub_response
|
57
|
+
else:
|
58
|
+
raise ValueError(
|
59
|
+
f"No text between tag {tag_start} and tag {tag_end} in response {response}!"
|
60
|
+
)
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# __init__.py
|
2
|
+
|
3
|
+
"""
|
4
|
+
This module contains classes for creating blocks and running workflows.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from .block import Block, log_and_check, log_and_check_with_memory
|
8
|
+
from .prompt import FormatPrompt
|
9
|
+
from .tool import GetMap, SencePOI, Tool
|
10
|
+
from .trigger import MemoryChangeTrigger, PortMessageTrigger
|
11
|
+
|
12
|
+
__all__ = [
|
13
|
+
"SencePOI",
|
14
|
+
"Tool",
|
15
|
+
"GetMap",
|
16
|
+
"MemoryChangeTrigger",
|
17
|
+
"PortMessageTrigger",
|
18
|
+
"Block",
|
19
|
+
"log_and_check",
|
20
|
+
"log_and_check_with_memory",
|
21
|
+
"FormatPrompt",
|
22
|
+
]
|
@@ -0,0 +1,137 @@
|
|
1
|
+
import asyncio
|
2
|
+
import functools
|
3
|
+
import inspect
|
4
|
+
import time
|
5
|
+
from typing import Any, Callable, Coroutine, Optional, Union
|
6
|
+
|
7
|
+
from ..llm import LLM
|
8
|
+
from ..memory import Memory
|
9
|
+
from ..utils.decorators import record_call_aio
|
10
|
+
|
11
|
+
TRIGGER_INTERVAL = 0.1
|
12
|
+
|
13
|
+
|
14
|
+
def log_and_check_with_memory(
|
15
|
+
condition: Union[
|
16
|
+
Callable[[Memory], Coroutine[Any, Any, bool]],
|
17
|
+
Callable[[], Coroutine[Any, Any, bool]],
|
18
|
+
Callable[[Memory], bool],
|
19
|
+
Callable[[], bool],
|
20
|
+
] = lambda: True,
|
21
|
+
trigger_interval: float = TRIGGER_INTERVAL,
|
22
|
+
record_function_calling: bool = False,
|
23
|
+
):
|
24
|
+
"""
|
25
|
+
A decorator that logs function calls and optionally checks a condition before executing the function.
|
26
|
+
|
27
|
+
This decorator is specifically designed to be used with the `block` method. A 'Memory' object is required in method input.
|
28
|
+
|
29
|
+
Args:
|
30
|
+
condition (Callable): A condition function that must be satisfied before the decorated function is executed.
|
31
|
+
Can be synchronous or asynchronous.
|
32
|
+
trigger_interval (float): The interval (in seconds) to wait between condition checks.
|
33
|
+
record_function_calling (bool): Whether to log the function call information.
|
34
|
+
"""
|
35
|
+
|
36
|
+
def decorator(func):
|
37
|
+
@record_call_aio(record_function_calling)
|
38
|
+
@functools.wraps(func)
|
39
|
+
async def wrapper(self, *args, **kwargs):
|
40
|
+
memory = None
|
41
|
+
for arg in list(args) + list(kwargs.values()):
|
42
|
+
if memory is not None:
|
43
|
+
break
|
44
|
+
if isinstance(arg, Memory):
|
45
|
+
memory = arg
|
46
|
+
assert isinstance(memory, Memory), "Input arguments has no `Memory` object!"
|
47
|
+
# Wait until the condition is met
|
48
|
+
sig = inspect.signature(condition)
|
49
|
+
params = list(sig.parameters.values())
|
50
|
+
if len(params) == 1 and params[0].annotation is Memory:
|
51
|
+
if inspect.iscoroutinefunction(condition):
|
52
|
+
while not await condition(memory): # type:ignore
|
53
|
+
await asyncio.sleep(trigger_interval)
|
54
|
+
else:
|
55
|
+
while not condition(memory): # type:ignore
|
56
|
+
await asyncio.sleep(trigger_interval)
|
57
|
+
elif len(params) == 0:
|
58
|
+
if inspect.iscoroutinefunction(condition):
|
59
|
+
while not await condition(): # type:ignore
|
60
|
+
await asyncio.sleep(trigger_interval)
|
61
|
+
else:
|
62
|
+
while not condition(): # type:ignore
|
63
|
+
await asyncio.sleep(trigger_interval)
|
64
|
+
else:
|
65
|
+
raise RuntimeError(
|
66
|
+
f"Invalid parameter format in condition function {condition}"
|
67
|
+
)
|
68
|
+
result = await func(self, *args, **kwargs)
|
69
|
+
return result
|
70
|
+
|
71
|
+
return wrapper
|
72
|
+
|
73
|
+
return decorator
|
74
|
+
|
75
|
+
|
76
|
+
def log_and_check(
|
77
|
+
condition: Union[
|
78
|
+
Callable[[], Coroutine[Any, Any, bool]],
|
79
|
+
Callable[[], bool],
|
80
|
+
] = lambda: True,
|
81
|
+
trigger_interval: float = TRIGGER_INTERVAL,
|
82
|
+
record_function_calling: bool = False,
|
83
|
+
):
|
84
|
+
"""
|
85
|
+
A decorator that logs function calls and optionally checks a condition before executing the function.
|
86
|
+
|
87
|
+
This decorator is specifically designed to be used with the `block` method.
|
88
|
+
|
89
|
+
Args:
|
90
|
+
condition (Callable): A condition function that must be satisfied before the decorated function is executed.
|
91
|
+
Can be synchronous or asynchronous.
|
92
|
+
trigger_interval (float): The interval (in seconds) to wait between condition checks.
|
93
|
+
record_function_calling (bool): Whether to log the function call information.
|
94
|
+
"""
|
95
|
+
|
96
|
+
def decorator(func):
|
97
|
+
@record_call_aio(record_function_calling)
|
98
|
+
@functools.wraps(func)
|
99
|
+
async def wrapper(self, *args, **kwargs):
|
100
|
+
# Wait until the condition is met
|
101
|
+
sig = inspect.signature(condition)
|
102
|
+
params = list(sig.parameters.values())
|
103
|
+
if len(params) == 0:
|
104
|
+
if inspect.iscoroutinefunction(condition):
|
105
|
+
while not await condition(): # type:ignore
|
106
|
+
await asyncio.sleep(trigger_interval)
|
107
|
+
else:
|
108
|
+
while not condition(): # type:ignore
|
109
|
+
await asyncio.sleep(trigger_interval)
|
110
|
+
else:
|
111
|
+
raise RuntimeError(
|
112
|
+
f"Invalid parameter format in condition function {condition}"
|
113
|
+
)
|
114
|
+
result = await func(self, *args, **kwargs)
|
115
|
+
return result
|
116
|
+
|
117
|
+
return wrapper
|
118
|
+
|
119
|
+
return decorator
|
120
|
+
|
121
|
+
|
122
|
+
# Define a Block, similar to a layer in PyTorch
|
123
|
+
class Block:
|
124
|
+
def __init__(
|
125
|
+
self,
|
126
|
+
name: str,
|
127
|
+
llm: Optional[LLM] = None,
|
128
|
+
):
|
129
|
+
self.name = name
|
130
|
+
self.llm = llm
|
131
|
+
|
132
|
+
async def reason(self, *args, **kwargs):
|
133
|
+
"""
|
134
|
+
Each block performs a specific reasoning task.
|
135
|
+
To be overridden by specific block implementations.
|
136
|
+
"""
|
137
|
+
raise NotImplementedError("Subclasses should implement this method")
|
@@ -0,0 +1,72 @@
|
|
1
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
2
|
+
import re
|
3
|
+
|
4
|
+
class FormatPrompt:
|
5
|
+
"""
|
6
|
+
A class to handle the formatting of prompts based on a template,
|
7
|
+
with support for system prompts and variable extraction.
|
8
|
+
|
9
|
+
Attributes:
|
10
|
+
template (str): The template string containing placeholders.
|
11
|
+
system_prompt (Optional[str]): An optional system prompt to add to the dialog.
|
12
|
+
variables (List[str]): A list of variable names extracted from the template.
|
13
|
+
formatted_string (str): The formatted string derived from the template and provided variables.
|
14
|
+
"""
|
15
|
+
|
16
|
+
def __init__(self, template: str, system_prompt: Optional[str] = None) -> None:
|
17
|
+
"""
|
18
|
+
Initializes the FormatPrompt with a template and an optional system prompt.
|
19
|
+
|
20
|
+
Args:
|
21
|
+
template (str): The string template with variable placeholders.
|
22
|
+
system_prompt (Optional[str]): An optional system prompt.
|
23
|
+
"""
|
24
|
+
self.template = template
|
25
|
+
self.system_prompt = system_prompt # Store the system prompt
|
26
|
+
self.variables = self._extract_variables()
|
27
|
+
self.formatted_string = '' # To store the formatted string
|
28
|
+
|
29
|
+
def _extract_variables(self) -> List[str]:
|
30
|
+
"""
|
31
|
+
Extracts variable names from the template string.
|
32
|
+
|
33
|
+
Returns:
|
34
|
+
List[str]: A list of variable names found within the template.
|
35
|
+
"""
|
36
|
+
return re.findall(r'\{(\w+)\}', self.template)
|
37
|
+
|
38
|
+
def format(self, **kwargs) -> str:
|
39
|
+
"""
|
40
|
+
Formats the template string using the provided keyword arguments.
|
41
|
+
|
42
|
+
Args:
|
43
|
+
**kwargs: Variable names and their corresponding values to format the template.
|
44
|
+
|
45
|
+
Returns:
|
46
|
+
str: The formatted string.
|
47
|
+
"""
|
48
|
+
self.formatted_string = self.template.format(**kwargs) # Store the formatted string
|
49
|
+
return self.formatted_string
|
50
|
+
|
51
|
+
def to_dialog(self) -> List[Dict[str, str]]:
|
52
|
+
"""
|
53
|
+
Converts the formatted prompt and optional system prompt into a dialog format.
|
54
|
+
|
55
|
+
Returns:
|
56
|
+
List[Dict[str, str]]: A list representing the dialog with roles and content.
|
57
|
+
"""
|
58
|
+
dialog = []
|
59
|
+
if self.system_prompt:
|
60
|
+
dialog.append({'role': 'system', 'content': self.system_prompt}) # Add system prompt if it exists
|
61
|
+
dialog.append({'role': 'user', 'content': self.formatted_string}) # Add user content
|
62
|
+
return dialog
|
63
|
+
|
64
|
+
def log(self) -> None:
|
65
|
+
"""
|
66
|
+
Logs the details of the FormatPrompt, including the template,
|
67
|
+
system prompt, extracted variables, and formatted string.
|
68
|
+
"""
|
69
|
+
print(f"FormatPrompt: {self.template}")
|
70
|
+
print(f"System Prompt: {self.system_prompt}") # Log the system prompt
|
71
|
+
print(f"Variables: {self.variables}")
|
72
|
+
print(f"Formatted String: {self.formatted_string}") # Log the formatted string
|
@@ -0,0 +1,246 @@
|
|
1
|
+
import asyncio
|
2
|
+
import logging
|
3
|
+
from copy import deepcopy
|
4
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
5
|
+
|
6
|
+
from mosstool.util.format_converter import dict2pb
|
7
|
+
from pycityproto.city.person.v2 import person_pb2 as person_pb2
|
8
|
+
|
9
|
+
from ..agent import Agent
|
10
|
+
from ..environment import (LEVEL_ONE_PRE, POI_TYPE_DICT, AoiService,
|
11
|
+
PersonService)
|
12
|
+
from ..workflow import Block
|
13
|
+
|
14
|
+
|
15
|
+
class Tool:
|
16
|
+
"""Abstract tool class for callable tools. Can be bound to an `Agent` or `Block` instance.
|
17
|
+
|
18
|
+
This class serves as a base for creating various tools that can perform different operations.
|
19
|
+
It is intended to be subclassed by specific tool implementations.
|
20
|
+
"""
|
21
|
+
|
22
|
+
def __get__(self, instance, owner):
|
23
|
+
if instance is None:
|
24
|
+
return self
|
25
|
+
subclass = type(self)
|
26
|
+
if not hasattr(instance, "_tools"):
|
27
|
+
instance._tools = {}
|
28
|
+
if subclass not in instance._tools:
|
29
|
+
tool_instance = subclass()
|
30
|
+
tool_instance._instance = instance # type: ignore
|
31
|
+
instance._tools[subclass] = tool_instance
|
32
|
+
return instance._tools[subclass]
|
33
|
+
|
34
|
+
def __call__(self, *args: Any, **kwds: Any) -> Any:
|
35
|
+
"""Invoke the tool's functionality.
|
36
|
+
|
37
|
+
This method must be implemented by subclasses to provide specific behavior.
|
38
|
+
"""
|
39
|
+
raise NotImplementedError
|
40
|
+
|
41
|
+
@property
|
42
|
+
def agent(self) -> Agent:
|
43
|
+
instance = self._instance # type:ignore
|
44
|
+
if not isinstance(instance, Agent):
|
45
|
+
raise RuntimeError(
|
46
|
+
f"Tool bind to object `{type(instance).__name__}`, not an `Agent` object!"
|
47
|
+
)
|
48
|
+
return instance
|
49
|
+
|
50
|
+
@property
|
51
|
+
def block(self) -> Block:
|
52
|
+
instance = self._instance # type:ignore
|
53
|
+
if not isinstance(instance, Block):
|
54
|
+
raise RuntimeError(
|
55
|
+
f"Tool bind to object `{type(instance).__name__}`, not an `Block` object!"
|
56
|
+
)
|
57
|
+
return instance
|
58
|
+
|
59
|
+
|
60
|
+
class GetMap(Tool):
|
61
|
+
"""Retrieve the map from the simulator. Can be bound only to an `Agent` instance."""
|
62
|
+
|
63
|
+
def __init__(self) -> None:
|
64
|
+
self.variables = []
|
65
|
+
|
66
|
+
async def __call__(self) -> Union[Any, Callable]:
|
67
|
+
agent = self.agent
|
68
|
+
if agent.simulator is None:
|
69
|
+
raise ValueError("Simulator is not set.")
|
70
|
+
return agent.simulator.map
|
71
|
+
|
72
|
+
|
73
|
+
class SencePOI(Tool):
|
74
|
+
"""Retrieve the Point of Interest (POI) of the current scene.
|
75
|
+
|
76
|
+
This tool computes the POI based on the current `position` stored in memory and returns
|
77
|
+
points of interest (POIs) within a specified radius. Can be bound only to an `Agent` instance.
|
78
|
+
|
79
|
+
Attributes:
|
80
|
+
radius (int): The radius within which to search for POIs.
|
81
|
+
category_prefix (str): The prefix for the categories of POIs to consider.
|
82
|
+
variables (List[str]): A list of variables relevant to the tool's operation.
|
83
|
+
|
84
|
+
Args:
|
85
|
+
radius (int, optional): The circular search radius. Defaults to 100.
|
86
|
+
category_prefix (str, optional): The category prefix to filter POIs. Defaults to LEVEL_ONE_PRE.
|
87
|
+
|
88
|
+
Methods:
|
89
|
+
__call__(radius: Optional[int] = None, category_prefix: Optional[str] = None) -> Union[Any, Callable]:
|
90
|
+
Executes the AOI retrieval operation, returning POIs based on the current state of memory and simulator.
|
91
|
+
"""
|
92
|
+
|
93
|
+
def __init__(self, radius: int = 100, category_prefix=LEVEL_ONE_PRE) -> None:
|
94
|
+
self.radius = radius
|
95
|
+
self.category_prefix = category_prefix
|
96
|
+
self.variables = ["position"]
|
97
|
+
|
98
|
+
async def __call__(
|
99
|
+
self, radius: Optional[int] = None, category_prefix: Optional[str] = None
|
100
|
+
) -> Union[Any, Callable]:
|
101
|
+
"""Retrieve the POIs within the specified radius and category prefix.
|
102
|
+
|
103
|
+
If both `radius` and `category_prefix` are None, the method will use the current position
|
104
|
+
from memory to query POIs using the simulator. Otherwise, it will return a new instance
|
105
|
+
of SenceAoi with the specified parameters.
|
106
|
+
|
107
|
+
Args:
|
108
|
+
radius (Optional[int]): A specific radius for the AOI query. If not provided, defaults to the instance's radius.
|
109
|
+
category_prefix (Optional[str]): A specific category prefix to filter POIs. If not provided, defaults to the instance's category_prefix.
|
110
|
+
|
111
|
+
Raises:
|
112
|
+
ValueError: If memory or simulator is not set.
|
113
|
+
|
114
|
+
Returns:
|
115
|
+
Union[Any, Callable]: The query results or a callable for a new SenceAoi instance.
|
116
|
+
"""
|
117
|
+
agent = self.agent
|
118
|
+
if agent.memory is None or agent.simulator is None:
|
119
|
+
raise ValueError("Memory or Simulator is not set.")
|
120
|
+
if radius is None and category_prefix is None:
|
121
|
+
position = await agent.memory.get("position")
|
122
|
+
resp = []
|
123
|
+
for prefix in self.category_prefix:
|
124
|
+
resp += agent.simulator.map.query_pois(
|
125
|
+
center=(position["xy_position"]["x"], position["xy_position"]["y"]),
|
126
|
+
radius=self.radius,
|
127
|
+
category_prefix=prefix,
|
128
|
+
)
|
129
|
+
# * Map six-digit codes to specific types
|
130
|
+
for poi in resp:
|
131
|
+
cate_str = poi[0]["category"]
|
132
|
+
poi[0]["category"] = POI_TYPE_DICT[cate_str]
|
133
|
+
else:
|
134
|
+
radius_ = radius if radius else self.radius
|
135
|
+
return SencePOI(radius_, category_prefix)
|
136
|
+
|
137
|
+
|
138
|
+
class UpdateWithSimulator(Tool):
|
139
|
+
def __init__(
|
140
|
+
self,
|
141
|
+
person_template_func: Callable[[], dict] = PersonService.default_dict_person,
|
142
|
+
) -> None:
|
143
|
+
self.person_template_func = person_template_func
|
144
|
+
|
145
|
+
async def _bind_to_simulator(
|
146
|
+
self,
|
147
|
+
):
|
148
|
+
"""
|
149
|
+
Bind Agent to Simulator
|
150
|
+
|
151
|
+
Args:
|
152
|
+
person_template (dict, optional): The person template in dict format. Defaults to PersonService.default_dict_person().
|
153
|
+
"""
|
154
|
+
agent = self.agent
|
155
|
+
if agent._simulator is None:
|
156
|
+
return
|
157
|
+
if not agent._has_bound_to_simulator:
|
158
|
+
FROM_MEMORY_KEYS = {
|
159
|
+
"attribute",
|
160
|
+
"home",
|
161
|
+
"work",
|
162
|
+
"vehicle_attribute",
|
163
|
+
"bus_attribute",
|
164
|
+
"pedestrian_attribute",
|
165
|
+
"bike_attribute",
|
166
|
+
}
|
167
|
+
simulator = agent.simulator
|
168
|
+
memory = agent.memory
|
169
|
+
person_id = await memory.get("id")
|
170
|
+
# ATTENTION:模拟器分配的id从0开始
|
171
|
+
if person_id >= 0:
|
172
|
+
await simulator.GetPerson(person_id)
|
173
|
+
logging.debug(f"Binding to Person `{person_id}` already in Simulator")
|
174
|
+
else:
|
175
|
+
dict_person = deepcopy(self.person_template_func())
|
176
|
+
for _key in FROM_MEMORY_KEYS:
|
177
|
+
try:
|
178
|
+
_value = await memory.get(_key)
|
179
|
+
if _value:
|
180
|
+
dict_person[_key] = _value
|
181
|
+
except KeyError as e:
|
182
|
+
continue
|
183
|
+
resp = await simulator.AddPerson(
|
184
|
+
dict2pb(dict_person, person_pb2.Person())
|
185
|
+
)
|
186
|
+
person_id = resp["person_id"]
|
187
|
+
await memory.update("id", person_id, protect_llm_read_only_fields=False)
|
188
|
+
logging.debug(
|
189
|
+
f"Binding to Person `{person_id}` just added to Simulator"
|
190
|
+
)
|
191
|
+
# 防止模拟器还没有到prepare阶段导致GetPerson出错
|
192
|
+
await asyncio.sleep(5)
|
193
|
+
agent._has_bound_to_simulator = True
|
194
|
+
else:
|
195
|
+
pass
|
196
|
+
|
197
|
+
async def _update_motion_with_sim(
|
198
|
+
self,
|
199
|
+
):
|
200
|
+
agent = self.agent
|
201
|
+
if agent._simulator is None:
|
202
|
+
return
|
203
|
+
if not agent._has_bound_to_simulator:
|
204
|
+
await self._bind_to_simulator()
|
205
|
+
simulator = agent.simulator
|
206
|
+
memory = agent.memory
|
207
|
+
person_id = await memory.get("id")
|
208
|
+
resp = await simulator.GetPerson(person_id)
|
209
|
+
resp_dict = resp["person"]
|
210
|
+
for k, v in resp_dict.get("motion", {}).items():
|
211
|
+
try:
|
212
|
+
await memory.get(k)
|
213
|
+
await memory.update(
|
214
|
+
k, v, mode="replace", protect_llm_read_only_fields=False
|
215
|
+
)
|
216
|
+
except KeyError as e:
|
217
|
+
continue
|
218
|
+
|
219
|
+
async def __call__(
|
220
|
+
self,
|
221
|
+
):
|
222
|
+
agent = self.agent
|
223
|
+
await self._bind_to_simulator()
|
224
|
+
await self._update_motion_with_sim()
|
225
|
+
|
226
|
+
|
227
|
+
class ResetAgentPosition(Tool):
|
228
|
+
def __init__(self) -> None:
|
229
|
+
pass
|
230
|
+
|
231
|
+
async def __call__(
|
232
|
+
self,
|
233
|
+
aoi_id: Optional[int] = None,
|
234
|
+
poi_id: Optional[int] = None,
|
235
|
+
lane_id: Optional[int] = None,
|
236
|
+
s: Optional[float] = None,
|
237
|
+
):
|
238
|
+
agent = self.agent
|
239
|
+
memory = agent.memory
|
240
|
+
await agent.simulator.ResetPersonPosition(
|
241
|
+
person_id=await memory.get("id"),
|
242
|
+
aoi_id=aoi_id,
|
243
|
+
poi_id=poi_id,
|
244
|
+
lane_id=lane_id,
|
245
|
+
s=s,
|
246
|
+
)
|