razaali10 commited on
Commit
0708dfc
·
verified ·
1 Parent(s): 0090fb6

Update tools.py

Browse files
Files changed (1) hide show
  1. tools.py +147 -30
tools.py CHANGED
@@ -58,39 +58,156 @@ def _require_results(session) -> None:
58
  # Model lifecycle
59
  # ---------------------------------------------------------------------------
60
 
61
- def upload_model(inp_content: str, filename: str = "model.inp") -> dict:
62
- """Upload an EPA SWMM .inp model (raw text or base64) and create a session.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
 
64
  Returns a session_id used by every other tool, plus element counts.
65
  """
66
- text = inp_content
67
- if "[" not in inp_content[:2000]: # likely base64
68
- try:
69
- text = base64.b64decode(inp_content, validate=True).decode("utf-8", errors="replace")
70
- except (binascii.Error, ValueError):
71
- pass
72
- if "[OPTIONS]" not in text.upper() and "[JUNCTIONS]" not in text.upper():
73
- raise ValueError("Content does not look like a SWMM .inp file (no [OPTIONS]/[JUNCTIONS] section).")
74
  session = STORE.create()
75
- safe_name = Path(filename).name or "model.inp"
76
- if not safe_name.lower().endswith(".inp"):
77
- safe_name += ".inp"
78
- inp_path = session.workdir / safe_name
79
- inp_path.write_text(text, encoding="utf-8")
80
- sections = mp.parse_inp_sections(str(inp_path))
81
- session.data.update({"filename": safe_name, "inp_path": str(inp_path), "sections": sections})
82
- counts = {name: len(rows) for name, rows in sections.items()
83
- if name in ("JUNCTIONS", "OUTFALLS", "STORAGE", "CONDUITS", "PUMPS", "WEIRS",
84
- "ORIFICES", "OUTLETS", "SUBCATCHMENTS", "RAINGAGES", "TIMESERIES")}
85
- gages = [row[0] for row in sections.get("RAINGAGES", []) if row]
86
- return {
87
- "session_id": session.id,
88
- "filename": safe_name,
89
- "element_counts": counts,
90
- "rain_gages": gages,
91
- "design_event_inference": infer_design_event(gages) if gages else None,
92
- "next_step": "Call run_simulation with this session_id.",
93
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
 
95
 
96
  def run_simulation(session_id: str) -> dict:
@@ -535,4 +652,4 @@ TOOL_REGISTRY: dict[str, Callable[..., dict]] = {
535
  calgary_screening, preliminary_design_review, get_reconciliation,
536
  run_scenario, attach_figure, set_report_details, generate_report,
537
  ]
538
- }
 
58
  # Model lifecycle
59
  # ---------------------------------------------------------------------------
60
 
61
+ MAX_MODEL_BYTES = 20 * 1024 * 1024
62
+
63
+
64
+ def _decode_model_payload(
65
+ inp_content: str,
66
+ filename: str = "model.inp",
67
+ encoding: str = "auto",
68
+ ) -> tuple[str, str]:
69
+ """Normalize raw text, base64, or a JSON-wrapped SWMM model payload."""
70
+ if not isinstance(inp_content, str) or not inp_content.strip():
71
+ raise ValueError("inp_content is required and must be a non-empty string.")
72
+
73
+ value = inp_content.strip()
74
+ resolved_filename = str(filename or "model.inp").strip() or "model.inp"
75
+ selected_encoding = str(encoding or "auto").strip().lower()
76
+
77
+ # Accept a complete JSON object passed as the inp_content value:
78
+ # {"filename": "...", "inp_content": "...", "encoding": "base64|raw|auto"}
79
+ if value.startswith("{"):
80
+ try:
81
+ payload = json.loads(value)
82
+ except json.JSONDecodeError:
83
+ payload = None
84
+
85
+ if isinstance(payload, dict):
86
+ if "inp_content" not in payload:
87
+ raise ValueError("JSON upload payload must contain an 'inp_content' field.")
88
+ value = str(payload.get("inp_content") or "").strip()
89
+ resolved_filename = (
90
+ str(payload.get("filename") or "").strip()
91
+ or resolved_filename
92
+ )
93
+ selected_encoding = str(
94
+ payload.get("encoding") or selected_encoding
95
+ ).strip().lower()
96
+
97
+ if not value:
98
+ raise ValueError("The upload payload contains no SWMM model content.")
99
+
100
+ if selected_encoding in {"raw", "text"}:
101
+ model_text = value
102
+
103
+ elif selected_encoding == "base64":
104
+ try:
105
+ decoded = base64.b64decode(value, validate=True)
106
+ except (binascii.Error, ValueError) as exc:
107
+ raise ValueError("Invalid base64 SWMM model content.") from exc
108
+ try:
109
+ model_text = decoded.decode("utf-8-sig")
110
+ except UnicodeDecodeError:
111
+ model_text = decoded.decode("cp1252")
112
+
113
+ elif selected_encoding == "auto":
114
+ # Raw INP text normally exposes a section header near the beginning.
115
+ if "[" in value[:2000]:
116
+ model_text = value
117
+ else:
118
+ try:
119
+ decoded = base64.b64decode(value, validate=True)
120
+ try:
121
+ candidate = decoded.decode("utf-8-sig")
122
+ except UnicodeDecodeError:
123
+ candidate = decoded.decode("cp1252")
124
+ except (binascii.Error, ValueError, UnicodeDecodeError):
125
+ candidate = ""
126
+
127
+ model_text = candidate if "[" in candidate[:2000] else value
128
+
129
+ else:
130
+ raise ValueError("encoding must be one of: auto, raw, text, or base64.")
131
+
132
+ encoded_size = len(model_text.encode("utf-8"))
133
+ if encoded_size > MAX_MODEL_BYTES:
134
+ raise ValueError(
135
+ f"SWMM model exceeds the {MAX_MODEL_BYTES // (1024 * 1024)} MB upload limit."
136
+ )
137
+
138
+ upper = model_text.upper()
139
+ if "[OPTIONS]" not in upper and "[JUNCTIONS]" not in upper:
140
+ raise ValueError(
141
+ "Content does not look like a SWMM .inp file "
142
+ "(no [OPTIONS] or [JUNCTIONS] section)."
143
+ )
144
+
145
+ safe_name = Path(resolved_filename).name or "model.inp"
146
+ if not safe_name.lower().endswith(".inp"):
147
+ safe_name = f"{Path(safe_name).stem}.inp"
148
+
149
+ return model_text, safe_name
150
+
151
+
152
+ def upload_model(
153
+ inp_content: str,
154
+ filename: str = "model.inp",
155
+ encoding: str = "auto",
156
+ ) -> dict:
157
+ """Upload an EPA SWMM model as raw text, base64, or a JSON-wrapped payload.
158
+
159
+ JSON payload format:
160
+ {"filename": "model.inp", "inp_content": "...", "encoding": "auto|raw|base64"}
161
 
162
  Returns a session_id used by every other tool, plus element counts.
163
  """
164
+ model_text, safe_name = _decode_model_payload(
165
+ inp_content=inp_content,
166
+ filename=filename,
167
+ encoding=encoding,
168
+ )
169
+
 
 
170
  session = STORE.create()
171
+ try:
172
+ inp_path = session.workdir / safe_name
173
+ inp_path.write_text(model_text, encoding="utf-8", newline="\n")
174
+
175
+ sections = mp.parse_inp_sections(str(inp_path))
176
+ if not sections:
177
+ raise ValueError("The uploaded file could not be parsed into SWMM sections.")
178
+
179
+ session.data.update({
180
+ "filename": safe_name,
181
+ "inp_path": str(inp_path),
182
+ "sections": sections,
183
+ "upload_encoding": encoding,
184
+ "model_size_bytes": inp_path.stat().st_size,
185
+ })
186
+
187
+ counts = {
188
+ name: len(rows)
189
+ for name, rows in sections.items()
190
+ if name in (
191
+ "JUNCTIONS", "OUTFALLS", "STORAGE", "CONDUITS", "PUMPS",
192
+ "WEIRS", "ORIFICES", "OUTLETS", "SUBCATCHMENTS",
193
+ "RAINGAGES", "TIMESERIES",
194
+ )
195
+ }
196
+ gages = [row[0] for row in sections.get("RAINGAGES", []) if row]
197
+
198
+ return {
199
+ "session_id": session.id,
200
+ "filename": safe_name,
201
+ "model_size_bytes": inp_path.stat().st_size,
202
+ "element_counts": counts,
203
+ "rain_gages": gages,
204
+ "design_event_inference": infer_design_event(gages) if gages else None,
205
+ "next_step": "Call run_simulation with this session_id.",
206
+ }
207
+
208
+ except Exception:
209
+ STORE.drop(session.id)
210
+ raise
211
 
212
 
213
  def run_simulation(session_id: str) -> dict:
 
652
  calgary_screening, preliminary_design_review, get_reconciliation,
653
  run_scenario, attach_figure, set_report_details, generate_report,
654
  ]
655
+ }