File indexing completed on 2026-04-27 07:41:42
0001 """
0002 PCS (Physics Configuration System) MCP tools — tag browsing and lookup.
0003
0004 Each tool registers with the MCP server and queries Django ORM via sync_to_async.
0005 """
0006
0007 from asgiref.sync import sync_to_async
0008 from mcp_server import mcp_server as mcp
0009
0010
0011 def _list_tags_sync(tag_type, category=None, status=None, creator=None,
0012 search=None):
0013 """List tags with filtering. Returns list of tag summaries."""
0014 from pcs.schemas import TAG_SCHEMAS, get_tag_model
0015
0016 if tag_type not in TAG_SCHEMAS:
0017 return {"error": f"Invalid tag_type '{tag_type}'. Use: p, e, s, r"}
0018
0019 model = get_tag_model(tag_type)
0020 qs = model.objects.order_by('-tag_number')
0021
0022 if tag_type == 'p':
0023 qs = qs.select_related('category')
0024 if category:
0025 qs = qs.filter(category__name__iexact=category)
0026
0027 if status:
0028 qs = qs.filter(status=status.lower())
0029 if creator:
0030 qs = qs.filter(created_by=creator)
0031
0032 if search:
0033 from django.db.models import Q
0034 q = Q(description__icontains=search) | Q(tag_label__icontains=search)
0035 qs = qs.filter(q)
0036
0037 tags = []
0038 for t in qs:
0039 entry = {
0040 'tag_label': t.tag_label,
0041 'status': t.status,
0042 'description': t.description,
0043 'created_by': t.created_by,
0044 'parameters': t.parameters,
0045 }
0046 if tag_type == 'p':
0047 entry['category'] = t.category.name
0048 tags.append(entry)
0049
0050 schema = TAG_SCHEMAS[tag_type]
0051 return {
0052 'tag_type': tag_type,
0053 'label': schema['label'],
0054 'count': len(tags),
0055 'tags': tags,
0056 }
0057
0058
0059 def _get_tag_sync(tag_label):
0060 """Get a single tag by label (e.g. 'p1001', 'e3', 'r1')."""
0061 label = tag_label.strip().lower()
0062 if not label or label[0] not in ('p', 'e', 's', 'r'):
0063 return {"error": f"Invalid tag label '{tag_label}'. Format: p1001, e3, s1, r1"}
0064
0065 prefix = label[0]
0066 try:
0067 number = int(label[1:])
0068 except ValueError:
0069 return {"error": f"Invalid tag number in '{tag_label}'"}
0070
0071 from pcs.schemas import get_tag_model
0072 model = get_tag_model(prefix)
0073
0074 try:
0075 t = model.objects.get(tag_number=number)
0076 except model.DoesNotExist:
0077 return {"error": f"Tag {tag_label} not found"}
0078
0079 result = {
0080 'tag_label': t.tag_label,
0081 'tag_number': t.tag_number,
0082 'status': t.status,
0083 'description': t.description,
0084 'parameters': t.parameters,
0085 'created_by': t.created_by,
0086 'created_at': t.created_at.isoformat(),
0087 }
0088 if prefix == 'p':
0089 t_with_cat = model.objects.select_related('category').get(tag_number=number)
0090 result['category'] = t_with_cat.category.name
0091 result['category_digit'] = t_with_cat.category.digit
0092
0093 return result
0094
0095
0096 def _search_tags_sync(query, tag_type=None):
0097 """Search across tag descriptions and parameters."""
0098 from pcs.schemas import TAG_SCHEMAS, get_tag_model
0099
0100 types = [tag_type] if tag_type else ['p', 'e', 's', 'r']
0101 results = []
0102
0103 for tt in types:
0104 if tt not in TAG_SCHEMAS:
0105 continue
0106 model = get_tag_model(tt)
0107 qs = model.objects.order_by('-tag_number')
0108 if tt == 'p':
0109 qs = qs.select_related('category')
0110
0111 q_lower = query.lower()
0112 for t in qs:
0113 searchable = ' '.join([
0114 t.tag_label, t.description,
0115 ' '.join(str(v) for v in t.parameters.values()),
0116 ]).lower()
0117 if q_lower in searchable:
0118 entry = {
0119 'tag_label': t.tag_label,
0120 'status': t.status,
0121 'description': t.description,
0122 'parameters': t.parameters,
0123 }
0124 if tt == 'p':
0125 entry['category'] = t.category.name
0126 results.append(entry)
0127
0128 return {
0129 'query': query,
0130 'count': len(results),
0131 'tags': results,
0132 }
0133
0134
0135 @mcp.tool()
0136 async def pcs_list_tags(
0137 tag_type: str,
0138 category: str = None,
0139 status: str = None,
0140 creator: str = None,
0141 search: str = None,
0142 ) -> dict:
0143 """
0144 List PCS tags (production task configurations) with optional filtering.
0145
0146 PCS tags capture physics process, event generation, simulation, and
0147 reconstruction configurations for ePIC Monte Carlo production campaigns.
0148
0149 Args:
0150 tag_type: Tag type — 'p' (physics), 'e' (evgen), 's' (simu), 'r' (reco). Required.
0151 category: Physics tags only — filter by category name (e.g. 'DIS', 'DVCS', 'EXCLUSIVE').
0152 status: Filter by status: 'draft' or 'locked'.
0153 creator: Filter by creator username.
0154 search: Text search in tag label and description.
0155
0156 Returns:
0157 tag_type, label, count, and list of tags with: tag_label, status,
0158 description, parameters, created_by, category (physics only).
0159 """
0160 return await sync_to_async(_list_tags_sync)(
0161 tag_type=tag_type, category=category, status=status,
0162 creator=creator, search=search,
0163 )
0164
0165
0166 @mcp.tool()
0167 async def pcs_get_tag(tag_label: str) -> dict:
0168 """
0169 Get full details of a single PCS tag by its label.
0170
0171 Args:
0172 tag_label: The tag label, e.g. 'p1001', 'e3', 's1', 'r1'.
0173 Case-insensitive.
0174
0175 Returns:
0176 tag_label, tag_number, status, description, parameters (all key-value
0177 pairs), created_by, created_at, and category/category_digit for physics tags.
0178 """
0179 return await sync_to_async(_get_tag_sync)(tag_label=tag_label)
0180
0181
0182 @mcp.tool()
0183 async def pcs_search_tags(
0184 query: str,
0185 tag_type: str = None,
0186 ) -> dict:
0187 """
0188 Search across PCS tags by text in label, description, or parameter values.
0189
0190 Use this when you don't know the exact tag label but know a keyword like
0191 'photoproduction', 'pythia8', 'eAu', or 'minQ2=1000'.
0192
0193 Args:
0194 query: Search text (case-insensitive). Matches against tag label,
0195 description, and all parameter values.
0196 tag_type: Optional — restrict to one type: 'p', 'e', 's', 'r'.
0197 If omitted, searches all tag types.
0198
0199 Returns:
0200 query, count, and list of matching tags with: tag_label, status,
0201 description, parameters, category (physics only).
0202 """
0203 return await sync_to_async(_search_tags_sync)(
0204 query=query, tag_type=tag_type,
0205 )