18
18
19
19
import re
20
20
from typing import AsyncGenerator
21
- from typing import Generator
22
21
from typing import TYPE_CHECKING
23
22
24
23
from typing_extensions import override
@@ -77,7 +76,52 @@ async def _populate_values(
77
76
instruction_template : str ,
78
77
context : InvocationContext ,
79
78
) -> str :
80
- """Populates values in the instruction template, e.g. state, artifact, etc."""
79
+ """Populates values in the instruction template, e.g. state, artifact, etc.
80
+
81
+ Supports nested dot-separated references like:
82
+ - state.user.name
83
+ - artifact.config.settings
84
+ - user.profile.email
85
+ - user?.profile?.name? (optional markers at any level)
86
+ """
87
+
88
+ def _get_nested_value (
89
+ obj , paths : list [str ], is_optional : bool = False
90
+ ) -> str :
91
+ """Gets a nested value from an object using a list of path segments.
92
+
93
+ Args:
94
+ obj: The object to get the value from
95
+ paths: List of path segments to traverse
96
+ is_optional: Whether the entire path is optional
97
+
98
+ Returns:
99
+ The value as a string
100
+
101
+ Raises:
102
+ KeyError: If the path doesn't exist and the reference is not optional
103
+ """
104
+ if not paths :
105
+ return str (obj )
106
+
107
+ # Get current part and remaining paths
108
+ current_part = paths [0 ]
109
+
110
+ # Handle optional markers
111
+ is_current_optional = current_part .endswith ('?' ) or is_optional
112
+ clean_part = current_part .removesuffix ('?' )
113
+
114
+ # Get value for current part
115
+ if isinstance (obj , dict ) and clean_part in obj :
116
+ return _get_nested_value (obj [clean_part ], paths [1 :], is_current_optional )
117
+ elif hasattr (obj , clean_part ):
118
+ return _get_nested_value (
119
+ getattr (obj , clean_part ), paths [1 :], is_current_optional
120
+ )
121
+ elif is_current_optional :
122
+ return ''
123
+ else :
124
+ raise KeyError (f'Key not found: { clean_part } ' )
81
125
82
126
async def _async_sub (pattern , repl_async_fn , string ) -> str :
83
127
result = []
@@ -96,29 +140,37 @@ async def _replace_match(match) -> str:
96
140
if var_name .endswith ('?' ):
97
141
optional = True
98
142
var_name = var_name .removesuffix ('?' )
99
- if var_name .startswith ('artifact.' ):
100
- var_name = var_name .removeprefix ('artifact.' )
101
- if context .artifact_service is None :
102
- raise ValueError ('Artifact service is not initialized.' )
103
- artifact = await context .artifact_service .load_artifact (
104
- app_name = context .session .app_name ,
105
- user_id = context .session .user_id ,
106
- session_id = context .session .id ,
107
- filename = var_name ,
108
- )
109
- if not var_name :
110
- raise KeyError (f'Artifact { var_name } not found.' )
111
- return str (artifact )
112
- else :
113
- if not _is_valid_state_name (var_name ):
114
- return match .group ()
115
- if var_name in context .session .state :
116
- return str (context .session .state [var_name ])
143
+
144
+ try :
145
+ if var_name .startswith ('artifact.' ):
146
+ var_name = var_name .removeprefix ('artifact.' )
147
+ if context .artifact_service is None :
148
+ raise ValueError ('Artifact service is not initialized.' )
149
+ artifact = await context .artifact_service .load_artifact (
150
+ app_name = context .session .app_name ,
151
+ user_id = context .session .user_id ,
152
+ session_id = context .session .id ,
153
+ filename = var_name ,
154
+ )
155
+ if not var_name :
156
+ raise KeyError (f'Artifact { var_name } not found.' )
157
+ return str (artifact )
117
158
else :
118
- if optional :
119
- return ''
120
- else :
159
+ if not _is_valid_state_name (var_name .split ('.' )[0 ].removesuffix ('?' )):
160
+ return match .group ()
161
+ # Try to resolve nested path
162
+ try :
163
+ return _get_nested_value (
164
+ context .session .state , var_name .split ('.' ), optional
165
+ )
166
+ except KeyError :
167
+ if not _is_valid_state_name (var_name ):
168
+ return match .group ()
121
169
raise KeyError (f'Context variable not found: `{ var_name } `.' )
170
+ except Exception as e :
171
+ if optional :
172
+ return ''
173
+ raise e
122
174
123
175
return await _async_sub (r'{+[^{}]*}+' , _replace_match , instruction_template )
124
176
0 commit comments